Skip to content

Commit 832d12f

Browse files
authored
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
1 parent 0011fcc commit 832d12f

File tree

6 files changed

+66
-33
lines changed

6 files changed

+66
-33
lines changed

config/release.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"mainBranch": "main",
44
"owner": "algolia",
55
"repo": "api-clients-automation",
6+
"teamSlug": "api-clients-automation",
67
"targetBranch": {
78
"javascript": "next",
89
"php": "next",

scripts/ci/codegen/upsertGenerationComment.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
/* eslint-disable no-console */
2-
import { Octokit } from '@octokit/rest';
3-
42
import { run } from '../../common';
5-
import { OWNER, REPO } from '../../release/common';
3+
import { getOctokit, OWNER, REPO } from '../../release/common';
64

75
import commentText from './text';
86

97
const BOT_NAME = 'algolia-bot';
108
const PR_NUMBER = parseInt(process.env.PR_NUMBER || '0', 10);
11-
const octokit = new Octokit({
12-
auth: `token ${process.env.GITHUB_TOKEN}`,
13-
});
9+
const octokit = getOctokit(process.env.GITHUB_TOKEN!);
1410

1511
const args = process.argv.slice(2);
1612
const allowedTriggers = ['notification', 'codegen', 'noGen', 'cleanup'];

scripts/release/common.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import path from 'path';
22

3+
import { Octokit } from '@octokit/rest';
4+
35
import clientsConfig from '../../config/clients.config.json';
46
import config from '../../config/release.config.json';
57
import { getGitHubUrl, run } from '../common';
@@ -8,6 +10,7 @@ export const RELEASED_TAG = config.releasedTag;
810
export const MAIN_BRANCH = config.mainBranch;
911
export const OWNER = config.owner;
1012
export const REPO = config.repo;
13+
export const TEAM_SLUG = config.teamSlug;
1114
export const MAIN_PACKAGE = Object.keys(clientsConfig).reduce(
1215
(mainPackage: { [lang: string]: string }, lang: string) => {
1316
return {
@@ -18,6 +21,12 @@ export const MAIN_PACKAGE = Object.keys(clientsConfig).reduce(
1821
{}
1922
);
2023

24+
export function getOctokit(githubToken: string): Octokit {
25+
return new Octokit({
26+
auth: `token ${githubToken}`,
27+
});
28+
}
29+
2130
export function getTargetBranch(language: string): string {
2231
return config.targetBranch[language] || config.defaultTargetBranch;
2332
}

scripts/release/create-release-issue.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
/* eslint-disable no-console */
2-
import { Octokit } from '@octokit/rest';
32
import dotenv from 'dotenv';
43
import semver from 'semver';
54

65
import { LANGUAGES, ROOT_ENV_PATH, run, getPackageVersion } from '../common';
76
import type { Language } from '../types';
87

9-
import { RELEASED_TAG, MAIN_BRANCH, OWNER, REPO, MAIN_PACKAGE } from './common';
8+
import {
9+
RELEASED_TAG,
10+
MAIN_BRANCH,
11+
OWNER,
12+
REPO,
13+
MAIN_PACKAGE,
14+
getOctokit,
15+
} from './common';
1016
import TEXT from './text';
1117
import type {
1218
Versions,
@@ -246,9 +252,7 @@ async function createReleaseIssue(): Promise<void> {
246252
TEXT.approval,
247253
].join('\n\n');
248254

249-
const octokit = new Octokit({
250-
auth: `token ${process.env.GITHUB_TOKEN}`,
251-
});
255+
const octokit = getOctokit(process.env.GITHUB_TOKEN!);
252256

253257
octokit.rest.issues
254258
.create({

scripts/release/process-release.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import {
2323
RELEASED_TAG,
2424
OWNER,
2525
REPO,
26+
TEAM_SLUG,
2627
getMarkdownSection,
2728
configureGitHubAuthor,
2829
cloneRepository,
30+
getOctokit,
2931
} from './common';
3032
import TEXT from './text';
3133
import type {
@@ -50,14 +52,22 @@ const BEFORE_CLIENT_COMMIT: { [lang: string]: BeforeClientCommitCommand } = {
5052
},
5153
};
5254

53-
function getIssueBody(): string {
54-
return JSON.parse(
55-
execa.sync('curl', [
56-
'-H',
57-
`Authorization: token ${process.env.GITHUB_TOKEN}`,
58-
`https://api.github.com/repos/${OWNER}/${REPO}/issues/${process.env.EVENT_NUMBER}`,
59-
]).stdout
60-
).body;
55+
async function getIssueBody(): Promise<string> {
56+
const octokit = getOctokit(process.env.GITHUB_TOKEN!);
57+
const {
58+
data: { body },
59+
} = await octokit.rest.issues.get({
60+
owner: OWNER,
61+
repo: REPO,
62+
issue_number: Number(process.env.EVENT_NUMBER),
63+
});
64+
65+
if (!body) {
66+
throw new Error(
67+
`Unexpected \`body\` of the release issue: ${JSON.stringify(body)}`
68+
);
69+
}
70+
return body;
6171
}
6272

6373
function getDateStamp(): string {
@@ -154,6 +164,26 @@ async function updateChangelog({
154164
);
155165
}
156166

167+
async function isAuthorizedRelease(): Promise<boolean> {
168+
const octokit = getOctokit(process.env.GITHUB_TOKEN!);
169+
const { data: members } = await octokit.rest.teams.listMembersInOrg({
170+
org: OWNER,
171+
team_slug: TEAM_SLUG,
172+
});
173+
174+
const { data: comments } = await octokit.rest.issues.listComments({
175+
owner: OWNER,
176+
repo: REPO,
177+
issue_number: Number(process.env.EVENT_NUMBER),
178+
});
179+
180+
return comments.some(
181+
(comment) =>
182+
comment.body?.toLowerCase().trim() === 'approved' &&
183+
members.find((member) => member.login === comment.user?.login)
184+
);
185+
}
186+
157187
async function processRelease(): Promise<void> {
158188
if (!process.env.GITHUB_TOKEN) {
159189
throw new Error('Environment variable `GITHUB_TOKEN` does not exist.');
@@ -163,16 +193,13 @@ async function processRelease(): Promise<void> {
163193
throw new Error('Environment variable `EVENT_NUMBER` does not exist.');
164194
}
165195

166-
const issueBody = getIssueBody();
167-
168-
if (
169-
!getMarkdownSection(issueBody, TEXT.approvalHeader)
170-
.split('\n')
171-
.find((line) => line.startsWith(`- [x] ${TEXT.approved}`))
172-
) {
173-
throw new Error('The issue was not approved.');
196+
if (!(await isAuthorizedRelease())) {
197+
throw new Error(
198+
'The issue was not approved.\nA team member must leave a comment "approved" in the release issue.'
199+
);
174200
}
175201

202+
const issueBody = await getIssueBody();
176203
const versionsToRelease = getVersionsToRelease(issueBody);
177204

178205
await updateOpenApiTools(versionsToRelease);

scripts/release/text.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const APPROVED = `Approved`;
2-
31
export default {
42
header: `## Summary`,
53

@@ -33,10 +31,8 @@ export default {
3331
changelogDescription: `Update the following lines. Once merged, it will be reflected to \`changelogs/*.\``,
3432

3533
approvalHeader: `## Approval`,
36-
approved: APPROVED,
3734
approval: [
38-
`To proceed this release, check the box below and close the issue.`,
39-
`To skip this release, just close the issue.`,
40-
`- [ ] ${APPROVED}`,
35+
`To proceed this release, a team member must leave a comment "approved" in this issue.`,
36+
`To skip this release, just close it.`,
4137
].join('\n'),
4238
};

0 commit comments

Comments
 (0)