Skip to content

Commit b2a1ea6

Browse files
authored
fix(release): accept exact generated merge title
1 parent ffb234b commit b2a1ea6

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

.github/workflows/release-trigger.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ jobs:
5959
const title = `release(version): Release ${version}`;
6060
if (pull.title !== title) throw new Error(`PR #${pullNumber} title does not match ${version}.`);
6161
62+
function isAllowedReleaseMergeSubject(message, expectedTitle, expectedPullNumber) {
63+
const subject = message.split('\n', 1)[0];
64+
return subject === expectedTitle ||
65+
subject === `${expectedTitle} (#${expectedPullNumber})`;
66+
}
67+
6268
const markerPattern = new RegExp(
6369
`<!-- podnotes-release-pr schema=1 version=${version.replaceAll('.', '\\.')}` +
6470
' base=([0-9a-f]{40}) -->',
@@ -92,7 +98,7 @@ jobs:
9298
if (releaseCommit.data.parents.length !== 1 || releaseCommit.data.parents[0].sha !== baseSha) {
9399
throw new Error(`PR #${pullNumber} was not squash-merged directly onto its recorded base.`);
94100
}
95-
if (releaseCommit.data.message.split('\n')[0] !== `${title} (#${pullNumber})`) {
101+
if (!isAllowedReleaseMergeSubject(releaseCommit.data.message, title, pullNumber)) {
96102
throw new Error(`PR #${pullNumber} merge commit message is invalid.`);
97103
}
98104
if (releaseCommit.data.tree.sha !== headCommit.data.tree.sha) {

.github/workflows/release.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ jobs:
6565
const title = `release(version): Release ${version}`;
6666
if (pull.title !== title) throw new Error(`PR #${pullNumber} title does not match ${version}.`);
6767
68+
function isAllowedReleaseMergeSubject(message, expectedTitle, expectedPullNumber) {
69+
const subject = message.split('\n', 1)[0];
70+
return subject === expectedTitle ||
71+
subject === `${expectedTitle} (#${expectedPullNumber})`;
72+
}
73+
6874
const markerPattern = new RegExp(
6975
`<!-- podnotes-release-pr schema=1 version=${version.replaceAll('.', '\\.')}` +
7076
' base=([0-9a-f]{40}) -->',
@@ -98,7 +104,7 @@ jobs:
98104
if (releaseCommit.data.parents.length !== 1 || releaseCommit.data.parents[0].sha !== baseSha) {
99105
throw new Error(`PR #${pullNumber} was not squash-merged directly onto its recorded base.`);
100106
}
101-
if (releaseCommit.data.message.split('\n')[0] !== `${title} (#${pullNumber})`) {
107+
if (!isAllowedReleaseMergeSubject(releaseCommit.data.message, title, pullNumber)) {
102108
throw new Error(`PR #${pullNumber} merge commit message is invalid.`);
103109
}
104110
if (releaseCommit.data.tree.sha !== headCommit.data.tree.sha) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import { runInNewContext } from "node:vm";
4+
import { describe, expect, it } from "vitest";
5+
6+
const RELEASE_TITLE = "release(version): Release 2.18.0";
7+
const RELEASE_PR = 265;
8+
const WORKFLOW_PATHS = [
9+
".github/workflows/release-trigger.yml",
10+
".github/workflows/release.yml",
11+
] as const;
12+
13+
type MergeSubjectValidator = (
14+
message: string,
15+
expectedTitle: string,
16+
expectedPullNumber: number,
17+
) => boolean;
18+
19+
async function readMergeSubjectValidator(workflowPath: string) {
20+
const workflow = await fs.readFile(path.resolve(workflowPath), "utf8");
21+
const functionStart = workflow.indexOf(" function isAllowedReleaseMergeSubject");
22+
expect(
23+
functionStart,
24+
`${workflowPath} must define the merge-subject validator`,
25+
).toBeGreaterThanOrEqual(0);
26+
const functionEnd = workflow.indexOf("\n }", functionStart);
27+
expect(functionEnd, `${workflowPath} must close the merge-subject validator`).toBeGreaterThan(
28+
functionStart,
29+
);
30+
const source = workflow
31+
.slice(functionStart, functionEnd + "\n }".length)
32+
.replace(/^ {12}/gm, "");
33+
return runInNewContext(`(${source})`) as MergeSubjectValidator;
34+
}
35+
36+
describe.each(WORKFLOW_PATHS)("%s release merge-subject contract", (workflowPath) => {
37+
it.each([
38+
[RELEASE_TITLE, true],
39+
[`${RELEASE_TITLE}\n\npodnotes-release-commit schema=1 version=2.18.0`, true],
40+
[`${RELEASE_TITLE} (#${RELEASE_PR})`, true],
41+
[`${RELEASE_TITLE} (#${RELEASE_PR})\n\nGitHub-generated body`, true],
42+
[`${RELEASE_TITLE} (#264)`, false],
43+
[`${RELEASE_TITLE} (#${RELEASE_PR}) extra`, false],
44+
[` ${RELEASE_TITLE}`, false],
45+
[`${RELEASE_TITLE} `, false],
46+
["release(version): Release 2.18.1", false],
47+
])("validates the exact merge subject in %j", async (message, expected) => {
48+
const validate = await readMergeSubjectValidator(workflowPath);
49+
expect(validate(message, RELEASE_TITLE, RELEASE_PR)).toBe(expected);
50+
});
51+
52+
it("uses the validator for the fetched release commit", async () => {
53+
const workflow = await fs.readFile(path.resolve(workflowPath), "utf8");
54+
expect(workflow).toContain(
55+
"if (!isAllowedReleaseMergeSubject(releaseCommit.data.message, title, pullNumber)) {",
56+
);
57+
});
58+
});

0 commit comments

Comments
 (0)