diff --git a/.github/workflows/lint-release-proposal.yml b/.github/workflows/lint-release-proposal.yml
index 1ea2b4b1b173e2..ecda2b616c0d02 100644
--- a/.github/workflows/lint-release-proposal.yml
+++ b/.github/workflows/lint-release-proposal.yml
@@ -33,30 +33,43 @@ jobs:
echo "COMMIT_SUBJECT=$COMMIT_SUBJECT" >> "$GITHUB_ENV"
- name: Lint release commit message trailers
run: |
- EXPECTED_TRAILER="^PR-URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/[[:digit:]]+\$"
+ EXPECTED_TRAILER="^$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/[[:digit:]]+\$"
echo "Expected trailer format: $EXPECTED_TRAILER"
- ACTUAL="$(git --no-pager log -1 --format=%b | git interpret-trailers --parse --no-divider)"
+ PR_URL="$(git --no-pager log -1 --format='%(trailers:key=PR-URL,valueonly)')"
echo "Actual: $ACTUAL"
- echo "$ACTUAL" | grep -E -q "$EXPECTED_TRAILER"
+ echo "$PR_URL" | grep -E -q "$EXPECTED_TRAILER"
- PR_URL="${ACTUAL:8}"
PR_HEAD="$(gh pr view "$PR_URL" --json headRefOid -q .headRefOid)"
echo "Head of $PR_URL: $PR_HEAD"
echo "Current commit: $GITHUB_SHA"
[ "$PR_HEAD" = "$GITHUB_SHA" ]
env:
GH_TOKEN: ${{ github.token }}
+ - name: Verify it's release-ready
+ run: |
+ SKIP_XZ=1 make release-only
- name: Validate CHANGELOG
id: releaser-info
run: |
EXPECTED_CHANGELOG_TITLE_INTRO="## $COMMIT_SUBJECT, @"
echo "Expected CHANGELOG section title: $EXPECTED_CHANGELOG_TITLE_INTRO"
- CHANGELOG_TITLE="$(grep "$EXPECTED_CHANGELOG_TITLE_INTRO" "doc/changelogs/CHANGELOG_V${COMMIT_SUBJECT:20:2}.md")"
+ MAJOR="$(awk '/^#define NODE_MAJOR_VERSION / { print $3 }' src/node_version.h)"
+ CHANGELOG_PATH="doc/changelogs/CHANGELOG_V${MAJOR}.md"
+ CHANGELOG_TITLE="$(grep "$EXPECTED_CHANGELOG_TITLE_INTRO" "$CHANGELOG_PATH")"
echo "Actual: $CHANGELOG_TITLE"
[ "${CHANGELOG_TITLE%%@*}@" = "$EXPECTED_CHANGELOG_TITLE_INTRO" ]
- - name: Verify NODE_VERSION_IS_RELEASE bit is correctly set
- run: |
- grep -q '^#define NODE_VERSION_IS_RELEASE 1$' src/node_version.h
- - name: Check for placeholders in documentation
- run: |
- ! grep "REPLACEME" doc/api/*.md
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ --jq '.commits.[] | { smallSha: .sha[0:10] } + (.commit.message|capture("^(?
.+)\n\n(.*\n)*PR-URL: (?.+)\n"))' \
+ "/repos/${GITHUB_REPOSITORY}/compare/v${MAJOR}.x...$GITHUB_SHA" --paginate \
+ | node tools/actions/lint-release-proposal-commit-list.mjs "$CHANGELOG_PATH" "$GITHUB_SHA" \
+ | while IFS= read -r PR_URL; do
+ LABEL="dont-land-on-v${MAJOR}.x" gh pr view \
+ --json labels,url \
+ --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' \
+ "$PR_URL" > /dev/null
+ done
+ 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.
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/tools/actions/lint-release-proposal-commit-list.mjs b/tools/actions/lint-release-proposal-commit-list.mjs
new file mode 100755
index 00000000000000..b9745bad3c30c1
--- /dev/null
+++ b/tools/actions/lint-release-proposal-commit-list.mjs
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+// Takes a stream of JSON objects as inputs, validates the CHANGELOG contains a
+// line corresponding, then outputs the prURL value.
+//
+// Example:
+// $ git log upstream/vXX.x...upstream/vX.X.X-proposal \
+// --format='{"prURL":"%(trailers:key=PR-URL,valueonly,separator=)","title":"%s","smallSha":"%h"}' \
+// | ./lint-release-proposal-commit-list.mjs "path/to/CHANGELOG.md" "$(git rev-parse upstream/vX.X.X-proposal)"
+
+const [,, CHANGELOG_PATH, RELEASE_COMMIT_SHA] = process.argv;
+
+import assert from 'node:assert';
+import { readFile } from 'node:fs/promises';
+import { createInterface } from 'node:readline';
+
+// Creating the iterator early to avoid missing any data:
+const stdinLineByLine = createInterface(process.stdin)[Symbol.asyncIterator]();
+
+const changelog = await readFile(CHANGELOG_PATH, 'utf-8');
+const commitListingStart = changelog.indexOf('\n### Commits\n');
+const commitListingEnd = changelog.indexOf('\n\n lineStart, `Commit title doesn't match`);
+ } catch (e) {
+ if (e?.code === 'ERR_ASSERTION') {
+ e.operator = 'includes';
+ e.expected = expectedCommitTitle;
+ e.actual = commitList.slice(lineStart + 1, lineEnd);
+ }
+ throw e;
+ }
+ assert.strictEqual(commitList.slice(lineEnd - prURL.length - 2, lineEnd), `(${prURL})`, `when checking ${smallSha} ${title}`);
+
+ expectedNumberOfCommitsLeft--;
+ console.log(prURL);
+}
+assert.strictEqual(expectedNumberOfCommitsLeft, 0, 'Release commit is not the last commit in the proposal');