◐ Shell
clean mode source ↗

tools: validate commit list as part of `lint-release-commit` · nodejs/node@4ff9aa7

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');