Skip to content

Commit 19de833

Browse files
authored
Use custom tooling to create the GitHub release, update reviewers (#59)
* Use custom tooling to create the GitHub release * Add todo * Remove unused libraries * Add changeset * Use compact GitHub changelog format * Use boolean * Fix comment detection for changeset bot
1 parent db1f037 commit 19de833

File tree

10 files changed

+250
-326
lines changed

10 files changed

+250
-326
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
33
"changelog": [
4-
"@changesets/changelog-github",
4+
"@svitejs/changesets-changelog-github-compact",
55
{ "repo": "gravitational/design-system" }
66
],
77
"commit": false,

.changeset/spotty-lamps-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gravitational/design-system': patch
3+
---
4+
5+
Another test to verify custom GitHub release

.github/workflows/release.yml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,25 @@ jobs:
5151
publish: pnpm release
5252
commit: "Version packages"
5353
commitMode: github-api
54+
createGithubReleases: false
5455
env:
5556
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
5657
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5758

58-
- name: Upload release asset
59+
- name: Extract version from Changesets
60+
id: meta
5961
if: steps.changesets.outputs.published == 'true'
6062
run: |
61-
set -e
62-
LATEST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName')
63-
gh release upload "$LATEST_TAG" dist/design-system.tgz
64-
echo "Uploaded $ASSET to the release $TAG"
63+
echo "version=${{ fromJSON(steps.changesets.outputs.publishedPackages)[0].version }}" >> "$GITHUB_OUTPUT"
64+
65+
- name: Create GitHub release
66+
if: steps.changesets.outputs.published == 'true'
6567
env:
6668
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
VERSION: ${{ steps.meta.outputs.version }}
70+
run: |
71+
pnpm bot release \
72+
${{ github.repository_owner }} \
73+
${{ github.event.repository.name }} \
74+
"$VERSION" \
75+
dist/design-system.tgz

bot/changeset.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,17 @@ async function getCommentId(
190190
octokit: Octokit,
191191
params: { repo: string; owner: string; issue_number: number }
192192
) {
193-
const comments = await octokit.rest.issues.listComments(params);
194-
const changesetBotComment = comments.data.find(
193+
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
194+
...params,
195+
per_page: 100,
196+
});
197+
198+
const changesetBotComment = comments.find(
195199
comment =>
196200
comment.user?.login === 'github-actions[bot]' &&
197-
(comment.body?.includes('No Changeset found') ??
198-
comment.body?.includes('Changeset detected'))
201+
/No Changeset found|Changeset detected/i.test(comment.body ?? '')
199202
);
203+
200204
return changesetBotComment ? changesetBotComment.id : null;
201205
}
202206

bot/index.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as core from '@actions/core';
22
import { Octokit } from '@octokit/rest';
33

44
import { runChangesetCommand } from './changeset.ts';
5+
import { runReleaseCommand } from './release.ts';
56
import { runReviewerCommand } from './review.ts';
67
import { runStorybookCommand } from './storybook.ts';
78
import { resolveErrorMessage } from './util.ts';
@@ -21,14 +22,6 @@ async function main() {
2122
process.exit(1);
2223
}
2324

24-
const [command, owner, repo, prNumber, action] = args;
25-
const pullNumber = parseInt(prNumber, 10);
26-
27-
if (isNaN(pullNumber)) {
28-
core.error('PR number must be a valid number');
29-
process.exit(1);
30-
}
31-
3225
const githubToken = process.env.GITHUB_TOKEN;
3326
if (!githubToken) {
3427
core.error('GITHUB_TOKEN environment variable is required');
@@ -39,6 +32,26 @@ async function main() {
3932
auth: githubToken,
4033
});
4134

35+
const [command, owner, repo, prNumber, action] = args;
36+
37+
// TODO(ryan): handle parameters a bit better so we don't have this special case
38+
if (command === 'release') {
39+
await runReleaseCommand(octokit, {
40+
owner,
41+
repo,
42+
version: args[2],
43+
tar_gz_path: args[3],
44+
});
45+
46+
return;
47+
}
48+
49+
const pullNumber = parseInt(prNumber, 10);
50+
if (isNaN(pullNumber)) {
51+
core.error('PR number must be a valid number');
52+
process.exit(1);
53+
}
54+
4255
try {
4356
switch (command) {
4457
case 'changeset':

bot/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
"@octokit/rest": "^22.0.0",
2222
"@octokit/webhooks": "^14.1.3",
2323
"human-id": "^4.1.1",
24-
"node-fetch": "^3.3.2"
24+
"mdast-util-to-string": "^4.0.0",
25+
"node-fetch": "^3.3.2",
26+
"remark-parse": "^11.0.0",
27+
"remark-stringify": "^11.0.0",
28+
"unified": "^11.0.5"
2529
},
2630
"devDependencies": {
2731
"typescript": "^5.9.2"

bot/release.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
import { Octokit } from '@octokit/rest';
4+
import { toString } from 'mdast-util-to-string';
5+
import remarkParse from 'remark-parse';
6+
import remarkStringify from 'remark-stringify';
7+
import { unified } from 'unified';
8+
9+
const BumpLevels = {
10+
dep: 0,
11+
patch: 1,
12+
minor: 2,
13+
major: 3,
14+
} as const;
15+
16+
export async function runReleaseCommand(
17+
octokit: Octokit,
18+
params: {
19+
owner: string;
20+
repo: string;
21+
version: string;
22+
tar_gz_path: string;
23+
}
24+
) {
25+
let changelog;
26+
try {
27+
changelog = await readFile('CHANGELOG.md', 'utf8');
28+
} catch (err) {
29+
if (isErrorWithCode(err, 'ENOENT')) {
30+
return;
31+
}
32+
33+
throw err;
34+
}
35+
36+
let changelogEntry = getChangelogEntry(changelog, params.version);
37+
if (!changelogEntry.content) {
38+
throw new Error(`Could not find changelog entry for ${params.version}`);
39+
}
40+
41+
const release = await octokit.rest.repos.createRelease({
42+
name: params.version,
43+
tag_name: `v${params.version}`,
44+
body: changelogEntry.content,
45+
prerelease: params.version.includes('-'),
46+
repo: params.repo,
47+
owner: params.owner,
48+
});
49+
50+
await octokit.rest.repos.uploadReleaseAsset({
51+
owner: params.owner,
52+
repo: params.repo,
53+
release_id: release.data.id,
54+
name: 'design-system.tar.gz',
55+
data: await readFile(params.tar_gz_path, 'utf-8'),
56+
});
57+
}
58+
59+
function isErrorWithCode(err: unknown, code: string) {
60+
return (
61+
typeof err === 'object' &&
62+
err !== null &&
63+
'code' in err &&
64+
err.code === code
65+
);
66+
}
67+
68+
interface ChangelogEntry {
69+
content: string;
70+
highestLevel: number;
71+
}
72+
73+
function getChangelogEntry(changelog: string, version: string): ChangelogEntry {
74+
let ast = unified().use(remarkParse).parse(changelog);
75+
76+
let highestLevel: number = BumpLevels.dep;
77+
78+
let nodes = ast.children;
79+
let headingStartInfo:
80+
| {
81+
index: number;
82+
depth: number;
83+
}
84+
| undefined;
85+
let endIndex: number | undefined;
86+
87+
for (let i = 0; i < nodes.length; i++) {
88+
let node = nodes[i];
89+
90+
if (node.type === 'heading') {
91+
let stringified: string = toString(node);
92+
let match = /(major|minor|patch)/.exec(stringified.toLowerCase());
93+
94+
if (match !== null) {
95+
let level = BumpLevels[match[0] as 'major' | 'minor' | 'patch'];
96+
highestLevel = Math.max(level, highestLevel);
97+
}
98+
99+
if (headingStartInfo === undefined && stringified === version) {
100+
headingStartInfo = {
101+
index: i,
102+
depth: node.depth,
103+
};
104+
105+
continue;
106+
}
107+
108+
if (
109+
endIndex === undefined &&
110+
headingStartInfo !== undefined &&
111+
headingStartInfo.depth === node.depth
112+
) {
113+
endIndex = i;
114+
115+
break;
116+
}
117+
}
118+
}
119+
120+
if (headingStartInfo) {
121+
ast.children = ast.children.slice(headingStartInfo.index + 1, endIndex);
122+
}
123+
124+
return {
125+
content: unified().use(remarkStringify).stringify(ast),
126+
highestLevel: highestLevel,
127+
};
128+
}

bot/review.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Octokit } from '@octokit/rest';
33

44
import { resolveErrorMessage } from './util.ts';
55

6-
const DESIGN_SYSTEM_REVIEWERS = ['ryanclark', 'strideynet'];
6+
const GROUP1_REVIEWERS = ['ryanclark'];
7+
const GROUP2_REVIEWERS = ['strideynet'];
78

89
export async function runReviewerCommand(
910
octokit: Octokit,
@@ -57,15 +58,15 @@ export async function runReviewerCommand(
5758
return;
5859
}
5960

60-
await assignRandomReviewer(octokit, {
61+
await assignRandomReviewers(octokit, {
6162
owner: params.owner,
6263
repo: params.repo,
6364
pull_number: params.pull_number,
6465
pr_author: prAuthor,
6566
});
6667
}
6768

68-
async function assignRandomReviewer(
69+
async function assignRandomReviewers(
6970
octokit: Octokit,
7071
params: {
7172
owner: string;
@@ -74,29 +75,45 @@ async function assignRandomReviewer(
7475
pr_author: string;
7576
}
7677
) {
77-
const eligibleReviewers = DESIGN_SYSTEM_REVIEWERS.filter(
78-
r => r !== params.pr_author
79-
);
78+
const eligibleGroup1 = GROUP1_REVIEWERS.filter(r => r !== params.pr_author);
79+
const eligibleGroup2 = GROUP2_REVIEWERS.filter(r => r !== params.pr_author);
80+
81+
const reviewers: string[] = [];
82+
83+
if (eligibleGroup1.length > 0) {
84+
const shuffledGroup1 = eligibleGroup1.toSorted(() => Math.random() - 0.5);
85+
reviewers.push(shuffledGroup1[0]);
86+
}
87+
88+
if (eligibleGroup2.length > 0) {
89+
const shuffledGroup2 = eligibleGroup2.toSorted(() => Math.random() - 0.5);
90+
const group2Count =
91+
eligibleGroup1.length === 0 ? Math.min(2, eligibleGroup2.length) : 1;
8092

81-
if (eligibleReviewers.length === 0) {
93+
reviewers.push(...shuffledGroup2.slice(0, group2Count));
94+
}
95+
96+
if (reviewers.length === 0) {
8297
core.warning('No eligible reviewers available');
8398
return;
8499
}
85100

86-
const randomIndex = Math.floor(Math.random() * eligibleReviewers.length);
87-
const selectedReviewer = eligibleReviewers[randomIndex];
88-
89101
try {
90102
await octokit.rest.pulls.requestReviewers({
91103
owner: params.owner,
92104
repo: params.repo,
93105
pull_number: params.pull_number,
94-
reviewers: [selectedReviewer],
106+
reviewers,
95107
});
96108

97-
core.info(`Successfully assigned reviewer: ${selectedReviewer}`);
109+
if (reviewers.length > 1) {
110+
core.info(`Successfully assigned reviewers: ${reviewers.join(' and ')}`);
111+
return reviewers;
112+
}
113+
114+
core.info(`Successfully assigned reviewer: ${reviewers[0]}`);
98115

99-
return selectedReviewer;
116+
return reviewers;
100117
} catch (error) {
101118
core.error(`Error assigning reviewer: ${resolveErrorMessage(error)}`);
102119

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"devDependencies": {
4444
"@chakra-ui/cli": "^3.26.0",
4545
"@chakra-ui/react": "^3.26.0",
46-
"@changesets/changelog-github": "^0.5.1",
4746
"@changesets/cli": "^2.29.7",
4847
"@changesets/get-release-plan": "^4.0.13",
4948
"@changesets/types": "^6.1.0",
@@ -59,6 +58,7 @@
5958
"@storybook/addon-themes": "^9.1.10",
6059
"@storybook/addon-vitest": "^9.1.10",
6160
"@storybook/react-vite": "^9.1.10",
61+
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
6262
"@swc/core": "^1.13.5",
6363
"@types/hast": "^3.0.4",
6464
"@types/prismjs": "^1.26.5",

0 commit comments

Comments
 (0)