diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0d047a071..aebcfb3aa 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -45,7 +45,9 @@ jobs: exit $diff - name: Lint json files - run: yarn eslint --ext=json . + run: | + yarn eslint --ext=json . + echo 'Use yarn fix:json to fix issues' outputs: RUN_SCRIPTS: ${{ steps.setup.outputs.RUN_SCRIPTS }} diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 000000000..e96311383 --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,22 @@ +name: Renovate + +on: + schedule: + - cron: '0 14 * * 5' + +jobs: + renovate: + name: Renovate + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup + id: setup + uses: ./.github/actions/setup + with: + type: minimal + + - run: yarn workspace scripts renovateWeeklyPR + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_RELEASE_BOT }} diff --git a/package.json b/package.json index 0bc1caa05..ccf8efb42 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "mustache": "4.2.0", "prettier": "2.6.2", "prettier-plugin-java": "1.6.1", + "renovate-config-algolia": "2.1.10", "typescript": "4.6.3" }, "engines": { diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..566d11c30 --- /dev/null +++ b/renovate.json @@ -0,0 +1,23 @@ +{ + "extends": [ + "config:js-app", + "algolia" + ], + "enabledManagers": [ + "npm", + "nvm" + ], + "baseBranches": [ + "chore/renovateBaseBranch" + ], + "packageRules": [ + { + "matchDepTypes": [ + "required_provider" + ], + "rangeStrategy": "bump" + } + ], + "prHourlyLimit": 10, + "prConcurrentLimit": 50 +} diff --git a/scripts/ci/codegen/__tests__/codegen.test.ts b/scripts/ci/codegen/__tests__/codegen.test.ts index 6d482f4e7..a00e57f75 100644 --- a/scripts/ci/codegen/__tests__/codegen.test.ts +++ b/scripts/ci/codegen/__tests__/codegen.test.ts @@ -21,6 +21,7 @@ describe('codegen', () => { describe('pushGeneratedCode', () => { it('throws without GITHUB_TOKEN environment variable', async () => { + process.env.GITHUB_TOKEN = ''; await expect(pushGeneratedCode()).rejects.toThrow( 'Environment variable `GITHUB_TOKEN` does not exist.' ); @@ -28,6 +29,10 @@ describe('codegen', () => { }); describe('upsertGenerationComment', () => { + beforeAll(() => { + process.env.GITHUB_TOKEN = 'mocked'; + }); + it('throws without parameter', async () => { await expect( // @ts-expect-error a parameter is required @@ -38,8 +43,6 @@ describe('codegen', () => { }); it('throws without PR_NUMBER environment variable', async () => { - process.env.GITHUB_TOKEN = 'foo'; - await expect(upsertGenerationComment('codegen')).rejects.toThrow( '`upsertGenerationComment` requires a `PR_NUMBER` environment variable.' ); diff --git a/scripts/ci/codegen/pushGeneratedCode.ts b/scripts/ci/codegen/pushGeneratedCode.ts index 466d43233..6f0cae377 100644 --- a/scripts/ci/codegen/pushGeneratedCode.ts +++ b/scripts/ci/codegen/pushGeneratedCode.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { MAIN_BRANCH, run } from '../../common'; +import { ensureGitHubToken, MAIN_BRANCH, run } from '../../common'; import { configureGitHubAuthor } from '../../release/common'; import { getNbGitDiff } from '../utils'; @@ -18,9 +18,7 @@ async function isUpToDate(baseBranch: string): Promise { * Push generated code for the current `JOB` and `CLIENT` on a `generated/` branch. */ export async function pushGeneratedCode(): Promise { - if (!process.env.GITHUB_TOKEN) { - throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); - } + ensureGitHubToken(); await configureGitHubAuthor(); diff --git a/scripts/ci/codegen/spreadGeneration.ts b/scripts/ci/codegen/spreadGeneration.ts index 1bfc53758..d3d56be91 100644 --- a/scripts/ci/codegen/spreadGeneration.ts +++ b/scripts/ci/codegen/spreadGeneration.ts @@ -8,6 +8,7 @@ import { run, toAbsolutePath, REPO_URL, + ensureGitHubToken, } from '../../common'; import { getLanguageFolder } from '../../config'; import { cloneRepository, configureGitHubAuthor } from '../../release/common'; @@ -60,9 +61,7 @@ export function cleanUpCommitMessage(commitMessage: string): string { } async function spreadGeneration(): Promise { - if (!process.env.GITHUB_TOKEN) { - throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); - } + const githubToken = ensureGitHubToken(); const lastCommitMessage = await run('git log -1 --format="%s"'); const author = ( @@ -81,7 +80,7 @@ async function spreadGeneration(): Promise { for (const lang of langs) { const { tempGitDir } = await cloneRepository({ lang, - githubToken: process.env.GITHUB_TOKEN, + githubToken, tempDir: process.env.RUNNER_TEMP!, }); diff --git a/scripts/ci/codegen/upsertGenerationComment.ts b/scripts/ci/codegen/upsertGenerationComment.ts index b9457566b..bc339ac9d 100644 --- a/scripts/ci/codegen/upsertGenerationComment.ts +++ b/scripts/ci/codegen/upsertGenerationComment.ts @@ -1,12 +1,10 @@ /* eslint-disable no-console */ -import { run, OWNER, REPO } from '../../common'; -import { getOctokit } from '../../release/common'; +import { run, OWNER, REPO, getOctokit } from '../../common'; import commentText from './text'; const BOT_NAME = 'algolia-bot'; const PR_NUMBER = parseInt(process.env.PR_NUMBER || '0', 10); -const octokit = getOctokit(process.env.GITHUB_TOKEN!); const args = process.argv.slice(2); const allowedTriggers = [ @@ -47,6 +45,7 @@ ${commentText[trigger].body( * Adds or updates a comment on a pull request. */ export async function upsertGenerationComment(trigger: Trigger): Promise { + const octokit = getOctokit(); if (!trigger || allowedTriggers.includes(trigger) === false) { throw new Error( `'upsertGenerationComment' requires a 'trigger' parameter (${allowedTriggers.join( diff --git a/scripts/ci/githubActions/renovateWeeklyPR.ts b/scripts/ci/githubActions/renovateWeeklyPR.ts new file mode 100644 index 000000000..546632f4c --- /dev/null +++ b/scripts/ci/githubActions/renovateWeeklyPR.ts @@ -0,0 +1,162 @@ +/* eslint-disable no-console */ +// script coming from crawler <3 +import type { Octokit } from '@octokit/rest'; + +import { getOctokit, OWNER, REPO, wait } from '../../common'; + +const BRANCH = 'chore/renovateBaseBranch'; +const BRANCH_BASE = 'main'; +const EMPTY_COMMIT_MSG = 'Automatic empty commit'; + +async function getRef( + octokit: Octokit, + branch: string +): Promise { + try { + const ref = await octokit.git.getRef({ + owner: OWNER, + repo: REPO, + ref: `heads/${branch}`, + }); + return ref.data.object.sha; + } catch (err) { + if (!(err instanceof Error) || (err as any).status !== 404) { + throw err; + } + } + return false; +} + +async function createBranch(octokit: Octokit, sha: string): Promise { + const create = await octokit.git.createRef({ + owner: OWNER, + repo: REPO, + ref: `refs/heads/${BRANCH}`, + sha, + }); + return create; +} + +async function deleteRef(octokit: Octokit): Promise { + console.log(`Deleting ref for ${BRANCH}`); + const ref = await octokit.git.deleteRef({ + owner: OWNER, + repo: REPO, + ref: `heads/${BRANCH}`, + }); + return ref; +} + +async function updateRef(octokit: Octokit, sha: string): Promise { + console.log(`Changing ref for ${BRANCH} to`, sha); + const ref = await octokit.git.updateRef({ + owner: OWNER, + repo: REPO, + ref: `heads/${BRANCH}`, + sha, + }); + return ref; +} + +async function getCommit(octokit: Octokit, sha: string): Promise { + const commit = await octokit.git.getCommit({ + owner: OWNER, + repo: REPO, + commit_sha: sha, + }); + return commit.data; +} + +function isCommitAnEmptyCommit(commit: any): boolean { + return commit.message.search(EMPTY_COMMIT_MSG) >= 0; +} + +async function createEmptyCommit( + octokit: Octokit, + refCommit: any +): Promise { + console.log('Creating empty commit'); + const commit = await octokit.git.createCommit({ + owner: OWNER, + repo: REPO, + message: EMPTY_COMMIT_MSG, + tree: refCommit.tree.sha, + parents: [refCommit.sha], + }); + return commit.data; +} + +async function createPR(octokit: Octokit): Promise { + // Next monday + const date = new Date(); + date.setDate(date.getDate() + 3); + + const title = `chore(scripts): dependencies ${ + date.toISOString().split('T')[0] + }`; + const { data } = await octokit.pulls.create({ + repo: REPO, + owner: OWNER, + title, + body: `Weekly dependencies update. +Contributes to #528 + `, + head: BRANCH, + base: BRANCH_BASE, + }); + return data; +} + +async function resetBranch( + octokit: Octokit, + refBase: string, + exists: boolean +): Promise { + if (exists) { + console.log('Deleting branch'); + await deleteRef(octokit); + await wait(5000); + } + + console.log('Creating branch'); + + await createBranch(octokit, refBase); + + const commit = await getCommit(octokit, refBase); + + const empty = await createEmptyCommit(octokit, commit); + await updateRef(octokit, empty.sha); +} + +(async (): Promise => { + try { + const octokit = getOctokit(); + + const refBase = await getRef(octokit, BRANCH_BASE); + const refTarget = await getRef(octokit, BRANCH); + console.log(BRANCH_BASE, 'is at', refBase); + console.log(BRANCH, 'is at', refTarget); + + if (!refBase) { + console.error('no sha for base branch'); + return; + } + + if (refTarget) { + console.log('Branch exists'); + const commit = await getCommit(octokit, refTarget); + + if (isCommitAnEmptyCommit(commit)) { + console.log('Empty commit exists'); + return; + } + } + + await resetBranch(octokit, refBase, Boolean(refTarget)); + + console.log('Creating pull request'); + await createPR(octokit); + } catch (err) { + console.error(err); + } +})(); diff --git a/scripts/common.ts b/scripts/common.ts index bf0bf875f..949ea8577 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -1,6 +1,7 @@ import fsp from 'fs/promises'; import path from 'path'; +import { Octokit } from '@octokit/rest'; import execa from 'execa'; // https://github.com/sindresorhus/execa/tree/v5.1.1 import { hashElement } from 'folder-hash'; import { remove } from 'fs-extra'; @@ -258,3 +259,27 @@ export async function runComposerUpdate(verbose: boolean): Promise { ); } } + +export function ensureGitHubToken(): string { + // use process.env here to mock with jest + if (!process.env.GITHUB_TOKEN) { + throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); + } + return process.env.GITHUB_TOKEN; +} + +export function getOctokit(): Octokit { + const token = ensureGitHubToken(); + return new Octokit({ + auth: `token ${token}`, + }); +} + +export function wait(waitTime: number): Promise { + if (waitTime <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => { + setTimeout(resolve, waitTime); + }); +} diff --git a/scripts/package.json b/scripts/package.json index a88f0e890..c76b869bb 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -8,6 +8,7 @@ "pre-commit": "./ci/husky/pre-commit.js", "processRelease": "ts-node release/process-release.ts", "pushGeneratedCode": "ts-node ci/codegen/pushGeneratedCode.ts", + "renovateWeeklyPR": "ts-node ci/githubActions/renovateWeeklyPR.ts", "setRunVariables": "ts-node ci/githubActions/setRunVariables.ts", "spreadGeneration": "ts-node ci/codegen/spreadGeneration.ts", "test": "jest", diff --git a/scripts/release/common.ts b/scripts/release/common.ts index c0c21ee30..1d0c0d6d9 100644 --- a/scripts/release/common.ts +++ b/scripts/release/common.ts @@ -1,7 +1,5 @@ import path from 'path'; -import { Octokit } from '@octokit/rest'; - import config from '../../config/release.config.json'; import { run } from '../common'; import { getGitHubUrl } from '../config'; @@ -10,12 +8,6 @@ import type { Language } from '../types'; export const RELEASED_TAG = config.releasedTag; export const TEAM_SLUG = config.teamSlug; -export function getOctokit(githubToken: string): Octokit { - return new Octokit({ - auth: `token ${githubToken}`, - }); -} - export function getTargetBranch(language: string): string { return config.targetBranch[language] || config.defaultTargetBranch; } diff --git a/scripts/release/create-release-issue.ts b/scripts/release/create-release-issue.ts index 82b175ff0..7e35b2bcc 100755 --- a/scripts/release/create-release-issue.ts +++ b/scripts/release/create-release-issue.ts @@ -10,10 +10,12 @@ import { MAIN_BRANCH, OWNER, REPO, + getOctokit, + ensureGitHubToken, } from '../common'; import { getPackageVersionDefault } from '../config'; -import { RELEASED_TAG, getOctokit } from './common'; +import { RELEASED_TAG } from './common'; import TEXT from './text'; import type { Versions, @@ -188,9 +190,7 @@ export function decideReleaseStrategy({ /* eslint-enable no-param-reassign */ async function createReleaseIssue(): Promise { - if (!process.env.GITHUB_TOKEN) { - throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); - } + ensureGitHubToken(); if ((await run('git rev-parse --abbrev-ref HEAD')) !== MAIN_BRANCH) { throw new Error( @@ -299,7 +299,7 @@ async function createReleaseIssue(): Promise { TEXT.approval, ].join('\n\n'); - const octokit = getOctokit(process.env.GITHUB_TOKEN); + const octokit = getOctokit(); octokit.rest.issues .create({ diff --git a/scripts/release/process-release.ts b/scripts/release/process-release.ts index 8645e3b20..347674d1e 100755 --- a/scripts/release/process-release.ts +++ b/scripts/release/process-release.ts @@ -19,6 +19,8 @@ import { emptyDirExceptForDotGit, GENERATORS, LANGUAGES, + getOctokit, + ensureGitHubToken, } from '../common'; import { getClientsConfigField, @@ -34,7 +36,6 @@ import { getMarkdownSection, configureGitHubAuthor, cloneRepository, - getOctokit, } from './common'; import TEXT from './text'; import type { @@ -61,7 +62,7 @@ const BEFORE_CLIENT_COMMIT: { [lang: string]: BeforeClientCommitCommand } = { }; async function getIssueBody(): Promise { - const octokit = getOctokit(process.env.GITHUB_TOKEN!); + const octokit = getOctokit(); const { data: { body }, } = await octokit.rest.issues.get({ @@ -230,7 +231,7 @@ function formatGitTag({ } async function isAuthorizedRelease(): Promise { - const octokit = getOctokit(process.env.GITHUB_TOKEN!); + const octokit = getOctokit(); const { data: members } = await octokit.rest.teams.listMembersInOrg({ org: OWNER, team_slug: TEAM_SLUG, @@ -250,9 +251,7 @@ async function isAuthorizedRelease(): Promise { } async function processRelease(): Promise { - if (!process.env.GITHUB_TOKEN) { - throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); - } + const githubToken = ensureGitHubToken(); if (!process.env.EVENT_NUMBER) { throw new Error('Environment variable `EVENT_NUMBER` does not exist.'); @@ -308,7 +307,7 @@ async function processRelease(): Promise { )) { const { tempGitDir } = await cloneRepository({ lang: lang as Language, - githubToken: process.env.GITHUB_TOKEN, + githubToken, tempDir: process.env.RUNNER_TEMP!, }); diff --git a/yarn.lock b/yarn.lock index e534c2e32..914b103ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,7 @@ __metadata: mustache: 4.2.0 prettier: 2.6.2 prettier-plugin-java: 1.6.1 + renovate-config-algolia: 2.1.10 typescript: 4.6.3 languageName: unknown linkType: soft @@ -18794,6 +18795,13 @@ __metadata: languageName: node linkType: hard +"renovate-config-algolia@npm:2.1.10": + version: 2.1.10 + resolution: "renovate-config-algolia@npm:2.1.10" + checksum: 2fa9e541ce8672c2dd96b0719ae0ca054e9e4fcbc24c9a4b7aee9047cd22f0f7d48f45142b65b41a7bb7da7738deee40a02920ce7893c6eabd24d40c5828ed3e + languageName: node + linkType: hard + "repeat-string@npm:^1.5.4": version: 1.6.1 resolution: "repeat-string@npm:1.6.1"