Skip to content

Commit 5f6785b

Browse files
committed
tools: validate commit list as part of lint-release-commit
PR-URL: #56291 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 00a1943 commit 5f6785b

File tree

2 files changed

+86
-11
lines changed

2 files changed

+86
-11
lines changed

.github/workflows/lint-release-proposal.yml

+24-11
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,43 @@ jobs:
3333
echo "COMMIT_SUBJECT=$COMMIT_SUBJECT" >> "$GITHUB_ENV"
3434
- name: Lint release commit message trailers
3535
run: |
36-
EXPECTED_TRAILER="^PR-URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/[[:digit:]]+\$"
36+
EXPECTED_TRAILER="^$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/[[:digit:]]+\$"
3737
echo "Expected trailer format: $EXPECTED_TRAILER"
38-
ACTUAL="$(git --no-pager log -1 --format=%b | git interpret-trailers --parse --no-divider)"
38+
PR_URL="$(git --no-pager log -1 --format='%(trailers:key=PR-URL,valueonly)')"
3939
echo "Actual: $ACTUAL"
40-
echo "$ACTUAL" | grep -E -q "$EXPECTED_TRAILER"
40+
echo "$PR_URL" | grep -E -q "$EXPECTED_TRAILER"
4141
42-
PR_URL="${ACTUAL:8}"
4342
PR_HEAD="$(gh pr view "$PR_URL" --json headRefOid -q .headRefOid)"
4443
echo "Head of $PR_URL: $PR_HEAD"
4544
echo "Current commit: $GITHUB_SHA"
4645
[ "$PR_HEAD" = "$GITHUB_SHA" ]
4746
env:
4847
GH_TOKEN: ${{ github.token }}
48+
- name: Verify it's release-ready
49+
run: |
50+
SKIP_XZ=1 make release-only
4951
- name: Validate CHANGELOG
5052
id: releaser-info
5153
run: |
5254
EXPECTED_CHANGELOG_TITLE_INTRO="## $COMMIT_SUBJECT, @"
5355
echo "Expected CHANGELOG section title: $EXPECTED_CHANGELOG_TITLE_INTRO"
54-
CHANGELOG_TITLE="$(grep "$EXPECTED_CHANGELOG_TITLE_INTRO" "doc/changelogs/CHANGELOG_V${COMMIT_SUBJECT:20:2}.md")"
56+
MAJOR="$(awk '/^#define NODE_MAJOR_VERSION / { print $3 }' src/node_version.h)"
57+
CHANGELOG_PATH="doc/changelogs/CHANGELOG_V${MAJOR}.md"
58+
CHANGELOG_TITLE="$(grep "$EXPECTED_CHANGELOG_TITLE_INTRO" "$CHANGELOG_PATH")"
5559
echo "Actual: $CHANGELOG_TITLE"
5660
[ "${CHANGELOG_TITLE%%@*}@" = "$EXPECTED_CHANGELOG_TITLE_INTRO" ]
57-
- name: Verify NODE_VERSION_IS_RELEASE bit is correctly set
58-
run: |
59-
grep -q '^#define NODE_VERSION_IS_RELEASE 1$' src/node_version.h
60-
- name: Check for placeholders in documentation
61-
run: |
62-
! grep "REPLACEME" doc/api/*.md
61+
gh api \
62+
-H "Accept: application/vnd.github+json" \
63+
-H "X-GitHub-Api-Version: 2022-11-28" \
64+
--jq '.commits.[] | { smallSha: .sha[0:10] } + (.commit.message|capture("^(?<title>.+)\n\n(.*\n)*PR-URL: (?<prURL>.+)\n"))' \
65+
"/repos/${GITHUB_REPOSITORY}/compare/v${MAJOR}.x...$GITHUB_SHA" --paginate \
66+
| node tools/actions/lint-release-proposal-commit-list.mjs "$CHANGELOG_PATH" "$GITHUB_SHA" \
67+
| while IFS= read -r PR_URL; do
68+
LABEL="dont-land-on-v${MAJOR}.x" gh pr view \
69+
--json labels,url \
70+
--jq 'if (.labels|map(.name==env.LABEL)|any) then error("\(.url) has the \(env.LABEL) label, forbidding it to be in this release proposal") end' \
71+
"$PR_URL" > /dev/null
72+
done
73+
shell: bash # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference, we want the pipefail option.
74+
env:
75+
GH_TOKEN: ${{ github.token }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env node
2+
3+
// Takes a stream of JSON objects as inputs, validates the CHANGELOG contains a
4+
// line corresponding, then outputs the prURL value.
5+
//
6+
// Example:
7+
// $ git log upstream/vXX.x...upstream/vX.X.X-proposal \
8+
// --format='{"prURL":"%(trailers:key=PR-URL,valueonly,separator=)","title":"%s","smallSha":"%h"}' \
9+
// | ./lint-release-proposal-commit-list.mjs "path/to/CHANGELOG.md" "$(git rev-parse upstream/vX.X.X-proposal)"
10+
11+
const [,, CHANGELOG_PATH, RELEASE_COMMIT_SHA] = process.argv;
12+
13+
import assert from 'node:assert';
14+
import { readFile } from 'node:fs/promises';
15+
import { createInterface } from 'node:readline';
16+
17+
// Creating the iterator early to avoid missing any data:
18+
const stdinLineByLine = createInterface(process.stdin)[Symbol.asyncIterator]();
19+
20+
const changelog = await readFile(CHANGELOG_PATH, 'utf-8');
21+
const commitListingStart = changelog.indexOf('\n### Commits\n');
22+
const commitListingEnd = changelog.indexOf('\n\n<a', commitListingStart);
23+
const commitList = changelog.slice(commitListingStart, commitListingEnd === -1 ? undefined : commitListingEnd + 1)
24+
// Checking for semverness is too expansive, it is left as a exercice for human reviewers.
25+
.replaceAll('**(SEMVER-MINOR)** ', '')
26+
// Correct Markdown escaping is validated by the linter, getting rid of it here helps.
27+
.replaceAll('\\', '');
28+
29+
let expectedNumberOfCommitsLeft = commitList.match(/\n\* \[/g).length;
30+
for await (const line of stdinLineByLine) {
31+
const { smallSha, title, prURL } = JSON.parse(line);
32+
33+
if (smallSha === RELEASE_COMMIT_SHA.slice(0, 10)) {
34+
assert.strictEqual(
35+
expectedNumberOfCommitsLeft, 0,
36+
'Some commits are listed without being included in the proposal, or are listed more than once',
37+
);
38+
continue;
39+
}
40+
41+
const lineStart = commitList.indexOf(`\n* [[\`${smallSha}\`]`);
42+
assert.notStrictEqual(lineStart, -1, `Cannot find ${smallSha} on the list`);
43+
const lineEnd = commitList.indexOf('\n', lineStart + 1);
44+
45+
const colonIndex = title.indexOf(':');
46+
const expectedCommitTitle = `${`**${title.slice(0, colonIndex)}`.replace('**Revert "', '_**Revert**_ "**')}**${title.slice(colonIndex)}`;
47+
try {
48+
assert(commitList.lastIndexOf(`/${smallSha})] - ${expectedCommitTitle} (`, lineEnd) > lineStart, `Commit title doesn't match`);
49+
} catch (e) {
50+
if (e?.code === 'ERR_ASSERTION') {
51+
e.operator = 'includes';
52+
e.expected = expectedCommitTitle;
53+
e.actual = commitList.slice(lineStart + 1, lineEnd);
54+
}
55+
throw e;
56+
}
57+
assert.strictEqual(commitList.slice(lineEnd - prURL.length - 2, lineEnd), `(${prURL})`, `when checking ${smallSha} ${title}`);
58+
59+
expectedNumberOfCommitsLeft--;
60+
console.log(prURL);
61+
}
62+
assert.strictEqual(expectedNumberOfCommitsLeft, 0, 'Release commit is not the last commit in the proposal');

0 commit comments

Comments
 (0)