Skip to content

Commit 73ea408

Browse files
committed
ci: sync auto release checkbox
1 parent 0eefc4d commit 73ea408

2 files changed

Lines changed: 181 additions & 1 deletion

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Changeset Auto Release Checkbox
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize, edited]
6+
pull_request_target:
7+
types: [opened, reopened, synchronize, edited]
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
jobs:
14+
sync-auto-release-checkbox:
15+
name: Sync auto-release checkbox
16+
runs-on: ubuntu-latest
17+
if: >-
18+
github.repository == 'udecode/kitcn' &&
19+
(
20+
(github.event_name == 'pull_request' &&
21+
github.event.pull_request.head.repo.full_name == github.repository) ||
22+
(github.event_name == 'pull_request_target' &&
23+
github.event.pull_request.head.repo.full_name != github.repository)
24+
)
25+
26+
steps:
27+
- name: 📥 Checkout workflow helper
28+
uses: actions/checkout@v4
29+
with:
30+
ref: >-
31+
${{
32+
github.event_name == 'pull_request'
33+
&& github.event.pull_request.head.sha
34+
|| github.event.repository.default_branch
35+
}}
36+
persist-credentials: false
37+
38+
- name: 🔁 Sync auto-release checkbox
39+
uses: actions/github-script@v7
40+
with:
41+
script: |
42+
const { pathToFileURL } = await import('node:url');
43+
const helperUrl = pathToFileURL(
44+
`${process.env.GITHUB_WORKSPACE}/tooling/auto-release-pr.mjs`
45+
).href;
46+
const {
47+
getChangesetReleaseType,
48+
shouldManageAutoReleaseBlock,
49+
upsertAutoReleaseBlock,
50+
} = await import(helperUrl);
51+
52+
const pullRequest = context.payload.pull_request;
53+
const files = await github.paginate(github.rest.pulls.listFiles, {
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
pull_number: pullRequest.number,
57+
per_page: 100,
58+
});
59+
60+
const hasChangeset = shouldManageAutoReleaseBlock({
61+
files,
62+
title: pullRequest.title ?? '',
63+
});
64+
const releaseType = hasChangeset
65+
? getChangesetReleaseType(files)
66+
: null;
67+
const currentBody = pullRequest.body ?? '';
68+
const nextBody = upsertAutoReleaseBlock(currentBody, {
69+
defaultChecked: releaseType === 'patch',
70+
hasChangeset,
71+
});
72+
73+
if (nextBody === currentBody) {
74+
core.info('PR body already matches auto-release policy.');
75+
return;
76+
}
77+
78+
await github.rest.pulls.update({
79+
owner: context.repo.owner,
80+
repo: context.repo.repo,
81+
pull_number: pullRequest.number,
82+
body: nextBody,
83+
});

tooling/auto-release-pr.mjs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
export const AUTO_RELEASE_START = '<!-- auto-release:start -->';
2+
export const AUTO_RELEASE_END = '<!-- auto-release:end -->';
3+
4+
const checkboxText = 'Auto release';
5+
const blockPattern = new RegExp(
6+
`${escapeRegExp(AUTO_RELEASE_START)}[\\s\\S]*?${escapeRegExp(AUTO_RELEASE_END)}\\n*`,
7+
'm'
8+
);
19
const checkedAutoReleasePattern = /-\s*\[[xX]\]\s*Auto release/;
10+
const changesetFrontmatterPattern = /^---\r?\n([\s\S]*?)\r?\n---/;
11+
const changesetReleaseTypePattern = /:\s*(major|minor|patch)\b/g;
12+
const versionPackagesTitlePattern = /\bVersion Packages\b/i;
213

314
export function hasChangesetFile(files) {
415
return files.some((file) => {
@@ -12,6 +23,92 @@ export function hasChangesetFile(files) {
1223
});
1324
}
1425

26+
export function getChangesetReleaseType(files) {
27+
const releaseTypes = files
28+
.filter(isChangesetFile)
29+
.flatMap((file) => parseChangesetReleaseTypes(getChangesetContent(file)));
30+
31+
if (releaseTypes.includes('major')) return 'major';
32+
if (releaseTypes.includes('minor')) return 'minor';
33+
if (releaseTypes.includes('patch')) return 'patch';
34+
35+
return null;
36+
}
37+
1538
export function isAutoReleaseChecked(body = '') {
16-
return checkedAutoReleasePattern.test(body);
39+
const block = getAutoReleaseBlock(body);
40+
41+
return checkedAutoReleasePattern.test(block);
42+
}
43+
44+
export function isVersionPackagesTitle(title = '') {
45+
return versionPackagesTitlePattern.test(title);
46+
}
47+
48+
export function shouldManageAutoReleaseBlock({ files, title }) {
49+
return !isVersionPackagesTitle(title) && hasChangesetFile(files);
50+
}
51+
52+
export function upsertAutoReleaseBlock(
53+
body,
54+
{ defaultChecked = false, hasChangeset }
55+
) {
56+
const bodyWithoutBlock = body.replace(blockPattern, '').trimEnd();
57+
58+
if (!hasChangeset) {
59+
return bodyWithoutBlock;
60+
}
61+
62+
const checked = getAutoReleaseBlock(body)
63+
? isAutoReleaseChecked(body)
64+
: defaultChecked;
65+
const checkbox = `- [${checked ? 'x' : ' '}] ${checkboxText}`;
66+
const block = `${AUTO_RELEASE_START}
67+
${checkbox}
68+
${AUTO_RELEASE_END}`;
69+
70+
return `${block}${bodyWithoutBlock ? `\n\n${bodyWithoutBlock}` : ''}\n`;
71+
}
72+
73+
function getAutoReleaseBlock(body) {
74+
return body.match(blockPattern)?.[0] ?? '';
75+
}
76+
77+
function getChangesetContent(file) {
78+
if (typeof file === 'string') return '';
79+
if (typeof file.contents === 'string') return file.contents;
80+
if (typeof file.content === 'string') return file.content;
81+
if (typeof file.patch === 'string') return getAddedPatchContent(file.patch);
82+
83+
return '';
84+
}
85+
86+
function getAddedPatchContent(patch) {
87+
return patch
88+
.split('\n')
89+
.filter((line) => line.startsWith('+') && !line.startsWith('+++'))
90+
.map((line) => line.slice(1))
91+
.join('\n');
92+
}
93+
94+
function isChangesetFile(file) {
95+
const filename = typeof file === 'string' ? file : file.filename;
96+
97+
return (
98+
filename?.startsWith('.changeset/') &&
99+
filename.endsWith('.md') &&
100+
filename !== '.changeset/README.md'
101+
);
102+
}
103+
104+
function parseChangesetReleaseTypes(content) {
105+
const frontmatter = content.match(changesetFrontmatterPattern)?.[1];
106+
107+
return [...(frontmatter ?? '').matchAll(changesetReleaseTypePattern)].map(
108+
(match) => match[1]
109+
);
110+
}
111+
112+
function escapeRegExp(value) {
113+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17114
}

0 commit comments

Comments
 (0)