Rewrite changelog script from Bash to JavaScript (#10782)

The `auto-changelog` script has been rewritten from Bash to JavaScript.
Functionally it should behave identically.
feature/default_network_editable
Mark Stacey 4 years ago committed by GitHub
parent b124ac29fc
commit b1dcbcec2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 86
      development/auto-changelog.js
  2. 60
      development/auto-changelog.sh
  3. 79
      development/lib/runCommand.js
  4. 2
      package.json

@ -0,0 +1,86 @@
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const runCommand = require('./lib/runCommand');
const URL = 'https://github.com/MetaMask/metamask-extension';
async function main() {
await runCommand('git', ['fetch', '--tags']);
const [mostRecentTagCommitHash] = await runCommand('git', [
'rev-list',
'--tags',
'--max-count=1',
]);
const [mostRecentTag] = await runCommand('git', [
'describe',
'--tags',
mostRecentTagCommitHash,
]);
const commitsSinceLastRelease = await runCommand('git', [
'rev-list',
`${mostRecentTag}..HEAD`,
]);
const changelogEntries = [];
for (const commit of commitsSinceLastRelease) {
const [subject] = await runCommand('git', [
'show',
'-s',
'--format=%s',
commit,
]);
let prefix;
let description = subject;
// Squash & Merge: the commit subject is parsed as `<description> (#<PR ID>)`
if (subject.match(/\(#\d+\)/u)) {
const [, prNumber] = subject.match(/\(#(\d+)\)/u);
prefix = `[#${prNumber}](${URL}/pull/${prNumber})`;
description = subject.match(/^(.+)\s\(#\d+\)/u)[1];
// Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request
// #<PR ID> from <branch>`, and the description is assumed to be the first line of the body.
// If no body is found, the description is set to the commit subject
} else if (subject.match(/#\d+\sfrom/u)) {
const [, prNumber] = subject.match(/#(\d+)\sfrom/u);
prefix = `[#${prNumber}](${URL}/pull/${prNumber})`;
const [firstLineOfBody] = await runCommand('git', [
'show',
'-s',
'--format=%b',
commit,
]);
description = firstLineOfBody || subject;
}
// Otherwise:
// Normal commits: The commit subject is the description, and the PR ID is omitted.
const changelogEntry = prefix
? `- ${prefix}: ${description}`
: `- ${description}`;
changelogEntries.push(changelogEntry);
}
const changelogFilename = path.resolve(__dirname, '..', 'CHANGELOG.md');
const changelog = await fs.readFile(changelogFilename, { encoding: 'utf8' });
const changelogLines = changelog.split('\n');
const releaseHeaderIndex = changelogLines.findIndex(
(line) => line === '## Current Develop Branch',
);
if (releaseHeaderIndex === -1) {
throw new Error('Failed to find release header');
}
changelogLines.splice(releaseHeaderIndex + 1, 0, ...changelogEntries);
const updatedChangelog = changelogLines.join('\n');
await fs.writeFile(changelogFilename, updatedChangelog);
console.log('CHANGELOG updated');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

@ -1,60 +0,0 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
readonly URL='https://github.com/MetaMask/metamask-extension'
git fetch --tags
most_recent_tag="$(git describe --tags "$(git rev-list --tags --max-count=1)")"
git rev-list "${most_recent_tag}"..HEAD | while read -r commit
do
subject="$(git show -s --format="%s" "$commit")"
# Squash & Merge: the commit subject is parsed as `<description> (#<PR ID>)`
if grep -E -q '\(#[[:digit:]]+\)' <<< "$subject"
then
pr="$(awk '{print $NF}' <<< "$subject" | tr -d '()')"
prefix="[$pr]($URL/pull/${pr###}): "
description="$(awk '{NF--; print $0}' <<< "$subject")"
# Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request
# #<PR ID> from <branch>`, and the description is assumed to be the first line of the body.
# If no body is found, the description is set to the commit subject
elif grep -E -q '#[[:digit:]]+\sfrom' <<< "$subject"
then
pr="$(awk '{print $4}' <<< "$subject")"
prefix="[$pr]($URL/pull/${pr###}): "
first_line_of_body="$(git show -s --format="%b" "$commit" | head -n 1 | tr -d '\r')"
if [[ -z "$first_line_of_body" ]]
then
description="$subject"
else
description="$first_line_of_body"
fi
# Normal commits: The commit subject is the description, and the PR ID is omitted.
else
pr=''
prefix=''
description="$subject"
fi
# add entry to CHANGELOG
if [[ "$OSTYPE" == "linux-gnu" ]]
then
# shellcheck disable=SC1004
sed -i'' '/## Current Develop Branch/a\
- '"$prefix$description"''$'\n' CHANGELOG.md
else
# shellcheck disable=SC1004
sed -i '' '/## Current Develop Branch/a\
- '"$prefix$description"''$'\n' CHANGELOG.md
fi
done
echo 'CHANGELOG updated'

@ -0,0 +1,79 @@
const spawn = require('cross-spawn');
/**
* Run a command to completion using the system shell.
*
* This will run a command with the specified arguments, and resolve when the
* process has exited. The STDOUT stream is monitored for output, which is
* returned after being split into lines. All output is expected to be UTF-8
* encoded, and empty lines are removed from the output.
*
* Anything received on STDERR is assumed to indicate a problem, and is tracked
* as an error.
*
* @param {string} command - The command to run
* @param {Array<string>} [args] - The arguments to pass to the command
* @returns {Array<string>} Lines of output received via STDOUT
*/
async function runCommand(command, args) {
const output = [];
let mostRecentError;
let errorSignal;
let errorCode;
const internalError = new Error('Internal');
try {
await new Promise((resolve, reject) => {
const childProcess = spawn(command, args, { encoding: 'utf8' });
childProcess.stdout.setEncoding('utf8');
childProcess.stderr.setEncoding('utf8');
childProcess.on('error', (error) => {
mostRecentError = error;
});
childProcess.stdout.on('data', (message) => {
const nonEmptyLines = message.split('\n').filter((line) => line !== '');
output.push(...nonEmptyLines);
});
childProcess.stderr.on('data', (message) => {
mostRecentError = new Error(message.trim());
});
childProcess.once('exit', (code, signal) => {
if (code === 0) {
return resolve();
}
errorCode = code;
errorSignal = signal;
return reject(internalError);
});
});
} catch (error) {
/**
* The error is re-thrown here in an `async` context to preserve the stack trace. If this was
* was thrown inside the Promise constructor, the stack trace would show a few frames of
* Node.js internals then end, without indicating where `runCommand` was called.
*/
if (error === internalError) {
let errorMessage;
if (errorCode !== null && errorSignal !== null) {
errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`;
} else if (errorSignal !== null) {
errorMessage = `Terminaled by signal '${errorSignal}'`;
} else if (errorCode === null) {
errorMessage = 'Exited with no code or signal';
} else {
errorMessage = `Exited with code '${errorCode}'`;
}
const improvedError = new Error(errorMessage);
if (mostRecentError) {
improvedError.cause = mostRecentError;
}
throw improvedError;
}
}
return output;
}
module.exports = runCommand;

@ -53,7 +53,7 @@
"storybook": "start-storybook -p 6006 -c .storybook --static-dir ./app ./storybook/images",
"storybook:build": "build-storybook -c .storybook -o storybook-build --static-dir ./app ./storybook/images",
"storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master",
"update-changelog": "./development/auto-changelog.sh",
"update-changelog": "node ./development/auto-changelog.js",
"generate:migration": "./development/generate-migration.sh",
"lavamoat:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
"lavamoat:debug": "lavamoat ./development/build/index.js --writeAutoPolicyDebug"

Loading…
Cancel
Save