From 832d12f1d8697b1bee8dd20208af3f12061a4997 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Fri, 1 Apr 2022 10:52:18 +0200 Subject: [PATCH] chore(ci): authenticate release issue by approval comment (#316) * chore(ci): authenticate release issue by approval comment * chore: fix lint error * chore: clean up * chore: validate comment body strictly * chore: early return if body does not exist * chore: extract octokit generation * chore: remove unused import --- config/release.config.json | 1 + scripts/ci/codegen/upsertGenerationComment.ts | 8 +-- scripts/release/common.ts | 9 +++ scripts/release/create-release-issue.ts | 14 +++-- scripts/release/process-release.ts | 59 ++++++++++++++----- scripts/release/text.ts | 8 +-- 6 files changed, 66 insertions(+), 33 deletions(-) diff --git a/config/release.config.json b/config/release.config.json index a2c6b29b0b..5c32612192 100644 --- a/config/release.config.json +++ b/config/release.config.json @@ -3,6 +3,7 @@ "mainBranch": "main", "owner": "algolia", "repo": "api-clients-automation", + "teamSlug": "api-clients-automation", "targetBranch": { "javascript": "next", "php": "next", diff --git a/scripts/ci/codegen/upsertGenerationComment.ts b/scripts/ci/codegen/upsertGenerationComment.ts index 47740d031d..2ca3c80358 100644 --- a/scripts/ci/codegen/upsertGenerationComment.ts +++ b/scripts/ci/codegen/upsertGenerationComment.ts @@ -1,16 +1,12 @@ /* eslint-disable no-console */ -import { Octokit } from '@octokit/rest'; - import { run } from '../../common'; -import { OWNER, REPO } from '../../release/common'; +import { getOctokit, OWNER, REPO } from '../../release/common'; import commentText from './text'; const BOT_NAME = 'algolia-bot'; const PR_NUMBER = parseInt(process.env.PR_NUMBER || '0', 10); -const octokit = new Octokit({ - auth: `token ${process.env.GITHUB_TOKEN}`, -}); +const octokit = getOctokit(process.env.GITHUB_TOKEN!); const args = process.argv.slice(2); const allowedTriggers = ['notification', 'codegen', 'noGen', 'cleanup']; diff --git a/scripts/release/common.ts b/scripts/release/common.ts index 029c3bc93e..ab0e6adf30 100644 --- a/scripts/release/common.ts +++ b/scripts/release/common.ts @@ -1,5 +1,7 @@ import path from 'path'; +import { Octokit } from '@octokit/rest'; + import clientsConfig from '../../config/clients.config.json'; import config from '../../config/release.config.json'; import { getGitHubUrl, run } from '../common'; @@ -8,6 +10,7 @@ export const RELEASED_TAG = config.releasedTag; export const MAIN_BRANCH = config.mainBranch; export const OWNER = config.owner; export const REPO = config.repo; +export const TEAM_SLUG = config.teamSlug; export const MAIN_PACKAGE = Object.keys(clientsConfig).reduce( (mainPackage: { [lang: string]: string }, lang: string) => { return { @@ -18,6 +21,12 @@ export const MAIN_PACKAGE = Object.keys(clientsConfig).reduce( {} ); +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 7ab7b7125f..568a3def77 100755 --- a/scripts/release/create-release-issue.ts +++ b/scripts/release/create-release-issue.ts @@ -1,12 +1,18 @@ /* eslint-disable no-console */ -import { Octokit } from '@octokit/rest'; import dotenv from 'dotenv'; import semver from 'semver'; import { LANGUAGES, ROOT_ENV_PATH, run, getPackageVersion } from '../common'; import type { Language } from '../types'; -import { RELEASED_TAG, MAIN_BRANCH, OWNER, REPO, MAIN_PACKAGE } from './common'; +import { + RELEASED_TAG, + MAIN_BRANCH, + OWNER, + REPO, + MAIN_PACKAGE, + getOctokit, +} from './common'; import TEXT from './text'; import type { Versions, @@ -246,9 +252,7 @@ async function createReleaseIssue(): Promise { TEXT.approval, ].join('\n\n'); - const octokit = new Octokit({ - auth: `token ${process.env.GITHUB_TOKEN}`, - }); + const octokit = getOctokit(process.env.GITHUB_TOKEN!); octokit.rest.issues .create({ diff --git a/scripts/release/process-release.ts b/scripts/release/process-release.ts index 23596e5b85..adc7fd214a 100755 --- a/scripts/release/process-release.ts +++ b/scripts/release/process-release.ts @@ -23,9 +23,11 @@ import { RELEASED_TAG, OWNER, REPO, + TEAM_SLUG, getMarkdownSection, configureGitHubAuthor, cloneRepository, + getOctokit, } from './common'; import TEXT from './text'; import type { @@ -50,14 +52,22 @@ const BEFORE_CLIENT_COMMIT: { [lang: string]: BeforeClientCommitCommand } = { }, }; -function getIssueBody(): string { - return JSON.parse( - execa.sync('curl', [ - '-H', - `Authorization: token ${process.env.GITHUB_TOKEN}`, - `https://api.github.com/repos/${OWNER}/${REPO}/issues/${process.env.EVENT_NUMBER}`, - ]).stdout - ).body; +async function getIssueBody(): Promise { + const octokit = getOctokit(process.env.GITHUB_TOKEN!); + const { + data: { body }, + } = await octokit.rest.issues.get({ + owner: OWNER, + repo: REPO, + issue_number: Number(process.env.EVENT_NUMBER), + }); + + if (!body) { + throw new Error( + `Unexpected \`body\` of the release issue: ${JSON.stringify(body)}` + ); + } + return body; } function getDateStamp(): string { @@ -154,6 +164,26 @@ async function updateChangelog({ ); } +async function isAuthorizedRelease(): Promise { + const octokit = getOctokit(process.env.GITHUB_TOKEN!); + const { data: members } = await octokit.rest.teams.listMembersInOrg({ + org: OWNER, + team_slug: TEAM_SLUG, + }); + + const { data: comments } = await octokit.rest.issues.listComments({ + owner: OWNER, + repo: REPO, + issue_number: Number(process.env.EVENT_NUMBER), + }); + + return comments.some( + (comment) => + comment.body?.toLowerCase().trim() === 'approved' && + members.find((member) => member.login === comment.user?.login) + ); +} + async function processRelease(): Promise { if (!process.env.GITHUB_TOKEN) { throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); @@ -163,16 +193,13 @@ async function processRelease(): Promise { throw new Error('Environment variable `EVENT_NUMBER` does not exist.'); } - const issueBody = getIssueBody(); - - if ( - !getMarkdownSection(issueBody, TEXT.approvalHeader) - .split('\n') - .find((line) => line.startsWith(`- [x] ${TEXT.approved}`)) - ) { - throw new Error('The issue was not approved.'); + if (!(await isAuthorizedRelease())) { + throw new Error( + 'The issue was not approved.\nA team member must leave a comment "approved" in the release issue.' + ); } + const issueBody = await getIssueBody(); const versionsToRelease = getVersionsToRelease(issueBody); await updateOpenApiTools(versionsToRelease); diff --git a/scripts/release/text.ts b/scripts/release/text.ts index 8dff3a4951..94c56e3cb2 100644 --- a/scripts/release/text.ts +++ b/scripts/release/text.ts @@ -1,5 +1,3 @@ -const APPROVED = `Approved`; - export default { header: `## Summary`, @@ -33,10 +31,8 @@ export default { changelogDescription: `Update the following lines. Once merged, it will be reflected to \`changelogs/*.\``, approvalHeader: `## Approval`, - approved: APPROVED, approval: [ - `To proceed this release, check the box below and close the issue.`, - `To skip this release, just close the issue.`, - `- [ ] ${APPROVED}`, + `To proceed this release, a team member must leave a comment "approved" in this issue.`, + `To skip this release, just close it.`, ].join('\n'), };