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
parent
b124ac29fc
commit
b1dcbcec2c
@ -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; |
Loading…
Reference in new issue