Repository: nodejs/node
Pull request: tools: add linter for release commit proposals

Currently, it's way too simple to forget about the `PR-URL: TODO` trailer – in fact, it has slipped through in some releases. Another thing that has happened to me is when postponing a release, I would remember to amend the date in the commit message but not in the CHANGELOG.
Let's add some automation that ensure we catch those kind of mistakes early.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: .github/workflows/lint-release-proposal.yml
patch: @@ -0,0 +1,59 @@
+name: Linters (release proposals)
+
+on:
+  push:
+    branches:
+      - v[0-9]+.[0-9]+.[0-9]+-proposal
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+env:
+  PYTHON_VERSION: '3.12'
+  NODE_VERSION: lts/*
+
+permissions:
+  contents: read
+
+jobs:
+  lint-release-commit:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
+        with:
+          persist-credentials: false
+      - name: Lint release commit title format
+        run: |
+          EXPECTED_TITLE='^[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}, Version [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ (\(Current|'.+' \(LTS)\)$'
+          echo "Expected commit title format: $EXPECTED_TITLE"
+          COMMIT_SUBJECT="$(git --no-pager log -1 --format=%s)"
+          echo "Actual: $ACTUAL"
+          echo "$COMMIT_SUBJECT" | grep -q -E "$EXPECTED_TITLE"
+          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:]]+\$"
+          echo "Expected trailer format: $EXPECTED_TRAILER"
+          ACTUAL="$(git --no-pager log -1 --format=%b | git interpret-trailers --parse --no-divider)"
+          echo "Actual: $ACTUAL"
+          echo "$ACTUAL" | 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: 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")"
+          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