|
| 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