mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Replace auto-changelog
script (#10993)
The `auto-changelog` script has been replaced with the package `@metamask/auto-changelog`. This package includes a script that has an `update` command that is roughly equivalent to the old `auto-changelog.js` script, except better. The script also has a `validate` command. The `repository` field was added to `package.json` because it's utilized by the `auto-changelog` script, and this was easier than specifying the repository URL with a CLI argument.
This commit is contained in:
parent
0e17ad3450
commit
20b0346d8b
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const path = require('path');
|
||||
const { version } = require('../app/manifest/_base.json');
|
||||
const { updateChangelog } = require('./lib/changelog/updateChangelog');
|
||||
const { unreleased } = require('./lib/changelog/constants');
|
||||
|
||||
const REPO_URL = 'https://github.com/MetaMask/metamask-extension';
|
||||
|
||||
const command = 'yarn update-changelog';
|
||||
|
||||
const helpText = `Usage: ${command} [--rc] [-h|--help]
|
||||
Update CHANGELOG.md with any changes made since the most recent release.
|
||||
|
||||
Options:
|
||||
--rc Add new changes to the current release header, rather than to the
|
||||
'${unreleased}' section.
|
||||
-h, --help Display this help and exit.
|
||||
|
||||
New commits will be added to the "${unreleased}" section (or to the section for the
|
||||
current release if the '--rc' flag is used) in reverse chronological order. Any
|
||||
commits for PRs that are represented already in the changelog will be ignored.
|
||||
|
||||
If the '--rc' flag is used and the section for the current release does not yet
|
||||
exist, it will be created.
|
||||
`;
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let isReleaseCandidate = false;
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--rc') {
|
||||
isReleaseCandidate = true;
|
||||
} else if (['--help', '-h'].includes(arg)) {
|
||||
console.log(helpText);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(
|
||||
`Unrecognized argument: ${arg}\nTry '${command} --help' for more information.\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const changelogFilename = path.resolve(__dirname, '..', 'CHANGELOG.md');
|
||||
const changelogContent = await fs.readFile(changelogFilename, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
const newChangelogContent = await updateChangelog({
|
||||
changelogContent,
|
||||
currentVersion: version,
|
||||
repoUrl: REPO_URL,
|
||||
isReleaseCandidate,
|
||||
});
|
||||
|
||||
await fs.writeFile(changelogFilename, newChangelogContent);
|
||||
|
||||
console.log('CHANGELOG updated');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
@ -1,305 +0,0 @@
|
||||
const semver = require('semver');
|
||||
|
||||
const { orderedChangeCategories, unreleased } = require('./constants');
|
||||
|
||||
const changelogTitle = '# Changelog';
|
||||
const changelogDescription = `All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).`;
|
||||
|
||||
// Stringification helpers
|
||||
|
||||
function stringifyCategory(category, changes) {
|
||||
const categoryHeader = `### ${category}`;
|
||||
if (changes.length === 0) {
|
||||
return categoryHeader;
|
||||
}
|
||||
const changeDescriptions = changes
|
||||
.map((description) => `- ${description}`)
|
||||
.join('\n');
|
||||
return `${categoryHeader}\n${changeDescriptions}`;
|
||||
}
|
||||
|
||||
function stringifyRelease(version, categories, { date, status } = {}) {
|
||||
const releaseHeader = `## [${version}]${date ? ` - ${date}` : ''}${
|
||||
status ? ` [${status}]` : ''
|
||||
}`;
|
||||
const categorizedChanges = orderedChangeCategories
|
||||
.filter((category) => categories[category])
|
||||
.map((category) => {
|
||||
const changes = categories[category];
|
||||
return stringifyCategory(category, changes);
|
||||
})
|
||||
.join('\n\n');
|
||||
if (categorizedChanges === '') {
|
||||
return releaseHeader;
|
||||
}
|
||||
return `${releaseHeader}\n${categorizedChanges}`;
|
||||
}
|
||||
|
||||
function stringifyReleases(releases, changes) {
|
||||
const stringifiedUnreleased = stringifyRelease(
|
||||
unreleased,
|
||||
changes[unreleased],
|
||||
);
|
||||
const stringifiedReleases = releases.map(({ version, date, status }) => {
|
||||
const categories = changes[version];
|
||||
return stringifyRelease(version, categories, { date, status });
|
||||
});
|
||||
|
||||
return [stringifiedUnreleased, ...stringifiedReleases].join('\n\n');
|
||||
}
|
||||
|
||||
function withTrailingSlash(url) {
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
}
|
||||
|
||||
function getCompareUrl(repoUrl, firstRef, secondRef) {
|
||||
return `${withTrailingSlash(repoUrl)}compare/${firstRef}...${secondRef}`;
|
||||
}
|
||||
|
||||
function getTagUrl(repoUrl, tag) {
|
||||
return `${withTrailingSlash(repoUrl)}releases/tag/${tag}`;
|
||||
}
|
||||
|
||||
function stringifyLinkReferenceDefinitions(repoUrl, releases) {
|
||||
const orderedReleases = releases
|
||||
.map(({ version }) => version)
|
||||
.sort((a, b) => semver.gt(a, b));
|
||||
|
||||
// The "Unreleased" section represents all changes made since the *highest*
|
||||
// release, not the most recent release. This is to accomodate patch releases
|
||||
// of older versions that don't represent the latest set of changes.
|
||||
//
|
||||
// For example, if a library has a v2.0.0 but the v1.0.0 release needed a
|
||||
// security update, the v1.0.1 release would then be the most recent, but the
|
||||
// range of unreleased changes would remain `v2.0.0...HEAD`.
|
||||
const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${getCompareUrl(
|
||||
repoUrl,
|
||||
`v${orderedReleases[0]}`,
|
||||
'HEAD',
|
||||
)}`;
|
||||
|
||||
// The "previous" release that should be used for comparison is not always
|
||||
// the most recent release chronologically. The _highest_ version that is
|
||||
// lower than the current release is used as the previous release, so that
|
||||
// patch releases on older releases can be accomodated.
|
||||
const releaseLinkReferenceDefinitions = releases
|
||||
.map(({ version }) => {
|
||||
if (version === orderedReleases[orderedReleases.length - 1]) {
|
||||
return `[${version}]: ${getTagUrl(repoUrl, `v${version}`)}`;
|
||||
}
|
||||
const versionIndex = orderedReleases.indexOf(version);
|
||||
const previousVersion = orderedReleases
|
||||
.slice(versionIndex)
|
||||
.find((releaseVersion) => {
|
||||
return semver.gt(version, releaseVersion);
|
||||
});
|
||||
return `[${version}]: ${getCompareUrl(
|
||||
repoUrl,
|
||||
`v${previousVersion}`,
|
||||
`v${version}`,
|
||||
)}`;
|
||||
})
|
||||
.join('\n');
|
||||
return `${unreleasedLinkReferenceDefinition}\n${releaseLinkReferenceDefinitions}${
|
||||
releases.length > 0 ? '\n' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./constants.js').Unreleased} Unreleased
|
||||
* @typedef {import('./constants.js').ChangeCategories ChangeCategories}
|
||||
*/
|
||||
/**
|
||||
* @typedef {import('./constants.js').Version} Version
|
||||
*/
|
||||
/**
|
||||
* Release metadata.
|
||||
* @typedef {Object} ReleaseMetadata
|
||||
* @property {string} date - An ISO-8601 formatted date, representing the
|
||||
* release date.
|
||||
* @property {string} status -The status of the release (e.g. 'WITHDRAWN', 'DEPRECATED')
|
||||
* @property {Version} version - The version of the current release.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Category changes. A list of changes in a single category.
|
||||
* @typedef {Array<string>} CategoryChanges
|
||||
*/
|
||||
|
||||
/**
|
||||
* Release changes, organized by category
|
||||
* @typedef {Record<keyof ChangeCategories, CategoryChanges>} ReleaseChanges
|
||||
*/
|
||||
|
||||
/**
|
||||
* Changelog changes, organized by release and by category.
|
||||
* @typedef {Record<Version|Unreleased, ReleaseChanges>} ChangelogChanges
|
||||
*/
|
||||
|
||||
/**
|
||||
* A changelog that complies with the ["keep a changelog" v1.1.0 guidelines]{@link https://keepachangelog.com/en/1.0.0/}.
|
||||
*
|
||||
* This changelog starts out completely empty, and allows new releases and
|
||||
* changes to be added such that the changelog remains compliant at all times.
|
||||
* This can be used to help validate the contents of a changelog, normalize
|
||||
* formatting, update a changelog, or build one from scratch.
|
||||
*/
|
||||
class Changelog {
|
||||
/**
|
||||
* Construct an empty changelog
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.repoUrl - The GitHub repository URL for the current project
|
||||
*/
|
||||
constructor({ repoUrl }) {
|
||||
this._releases = [];
|
||||
this._changes = { [unreleased]: {} };
|
||||
this._repoUrl = repoUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a release to the changelog
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.addToStart] - Determines whether the release is
|
||||
* added to the top or bottom of the changelog. This defaults to 'true'
|
||||
* because new releases should be added to the top of the changelog. This
|
||||
* should be set to 'false' when parsing a changelog top-to-bottom.
|
||||
* @param {string} [options.date] - An ISO-8601 formatted date, representing the
|
||||
* release date.
|
||||
* @param {string} [options.status] - The status of the release (e.g.
|
||||
* 'WITHDRAWN', 'DEPRECATED')
|
||||
* @param {Version} options.version - The version of the current release,
|
||||
* which should be a [semver]{@link https://semver.org/spec/v2.0.0.html}-
|
||||
* compatible version.
|
||||
*/
|
||||
addRelease({ addToStart = true, date, status, version }) {
|
||||
if (!version) {
|
||||
throw new Error('Version required');
|
||||
} else if (semver.valid(version) === null) {
|
||||
throw new Error(`Not a valid semver version: '${version}'`);
|
||||
} else if (this._changes[version]) {
|
||||
throw new Error(`Release already exists: '${version}'`);
|
||||
}
|
||||
|
||||
this._changes[version] = {};
|
||||
const newRelease = { version, date, status };
|
||||
if (addToStart) {
|
||||
this._releases.unshift(newRelease);
|
||||
} else {
|
||||
this._releases.push(newRelease);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a change to the changelog
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.addToStart] - Determines whether the change is
|
||||
* added to the top or bottom of the list of changes in this category. This
|
||||
* defaults to 'true' because changes should be in reverse-chronological
|
||||
* order. This should be set to 'false' when parsing a changelog top-to-
|
||||
* bottom.
|
||||
* @param {string} options.category - The category of the change.
|
||||
* @param {string} options.description - The description of the change.
|
||||
* @param {Version} [options.version] - The version this change was released
|
||||
* in. If this is not given, the change is assumed to be unreleased.
|
||||
*/
|
||||
addChange({ addToStart = true, category, description, version }) {
|
||||
if (!category) {
|
||||
throw new Error('Category required');
|
||||
} else if (!orderedChangeCategories.includes(category)) {
|
||||
throw new Error(`Unrecognized category: '${category}'`);
|
||||
} else if (!description) {
|
||||
throw new Error('Description required');
|
||||
} else if (version !== undefined && !this._changes[version]) {
|
||||
throw new Error(`Specified release version does not exist: '${version}'`);
|
||||
}
|
||||
|
||||
const release = version
|
||||
? this._changes[version]
|
||||
: this._changes[unreleased];
|
||||
|
||||
if (!release[category]) {
|
||||
release[category] = [];
|
||||
}
|
||||
if (addToStart) {
|
||||
release[category].unshift(description);
|
||||
} else {
|
||||
release[category].push(description);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all unreleased changes to a release section.
|
||||
*
|
||||
* Changes are migrated in their existing categories, and placed above any
|
||||
* pre-existing changes in that category.
|
||||
*
|
||||
* @param {Version} version - The release version to migrate unreleased
|
||||
* changes to.
|
||||
*/
|
||||
migrateUnreleasedChangesToRelease(version) {
|
||||
const releaseChanges = this._changes[version];
|
||||
if (!releaseChanges) {
|
||||
throw new Error(`Specified release version does not exist: '${version}'`);
|
||||
}
|
||||
|
||||
const unreleasedChanges = this._changes[unreleased];
|
||||
|
||||
for (const category of Object.keys(unreleasedChanges)) {
|
||||
if (releaseChanges[category]) {
|
||||
releaseChanges[category] = [
|
||||
...unreleasedChanges[category],
|
||||
...releaseChanges[category],
|
||||
];
|
||||
} else {
|
||||
releaseChanges[category] = unreleasedChanges[category];
|
||||
}
|
||||
}
|
||||
this._changes[unreleased] = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata for all releases.
|
||||
* @returns {Array<ReleaseMetadata>} The metadata for each release.
|
||||
*/
|
||||
getReleases() {
|
||||
return this._releases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the changes in the given release, organized by category.
|
||||
* @param {Version} version - The version of the release being retrieved.
|
||||
* @returns {ReleaseChanges} The changes included in the given released.
|
||||
*/
|
||||
getReleaseChanges(version) {
|
||||
return this._changes[version];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all changes that have not yet been released
|
||||
* @returns {ReleaseChanges} The changes that have not yet been released.
|
||||
*/
|
||||
getUnreleasedChanges() {
|
||||
return this._changes[unreleased];
|
||||
}
|
||||
|
||||
/**
|
||||
* The stringified changelog, formatted according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
* @returns {string} The stringified changelog.
|
||||
*/
|
||||
toString() {
|
||||
return `${changelogTitle}
|
||||
${changelogDescription}
|
||||
|
||||
${stringifyReleases(this._releases, this._changes)}
|
||||
|
||||
${stringifyLinkReferenceDefinitions(this._repoUrl, this._releases)}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Changelog;
|
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Version string
|
||||
* @typedef {string} Version - A [SemVer]{@link https://semver.org/spec/v2.0.0.html}-
|
||||
* compatible version string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Change categories.
|
||||
*
|
||||
* Most of these categories are from [Keep a Changelog]{@link https://keepachangelog.com/en/1.0.0/}.
|
||||
* The "Uncategorized" category was added because we have many changes from
|
||||
* older releases that would be difficult to categorize.
|
||||
*
|
||||
* @typedef {Record<string, string>} ChangeCategories
|
||||
* @property {'Added'} Added - for new features.
|
||||
* @property {'Changed'} Changed - for changes in existing functionality.
|
||||
* @property {'Deprecated'} Deprecated - for soon-to-be removed features.
|
||||
* @property {'Fixed'} Fixed - for any bug fixes.
|
||||
* @property {'Removed'} Removed - for now removed features.
|
||||
* @property {'Security'} Security - in case of vulnerabilities.
|
||||
* @property {'Uncategorized'} Uncategorized - for any changes that have not
|
||||
* yet been categorized.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {ChangeCategories}
|
||||
*/
|
||||
const changeCategories = {
|
||||
Added: 'Added',
|
||||
Changed: 'Changed',
|
||||
Deprecated: 'Deprecated',
|
||||
Fixed: 'Fixed',
|
||||
Removed: 'Removed',
|
||||
Security: 'Security',
|
||||
Uncategorized: 'Uncategorized',
|
||||
};
|
||||
|
||||
/**
|
||||
* Change categories in the order in which they should be listed in the
|
||||
* changelog.
|
||||
*
|
||||
* @type {Array<keyof ChangeCategories>}
|
||||
*/
|
||||
const orderedChangeCategories = [
|
||||
'Uncategorized',
|
||||
'Added',
|
||||
'Changed',
|
||||
'Deprecated',
|
||||
'Removed',
|
||||
'Fixed',
|
||||
'Security',
|
||||
];
|
||||
|
||||
/**
|
||||
* The header for the section of the changelog listing unreleased changes.
|
||||
* @typedef {'Unreleased'} Unreleased
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Unreleased}
|
||||
*/
|
||||
const unreleased = 'Unreleased';
|
||||
|
||||
module.exports = {
|
||||
changeCategories,
|
||||
orderedChangeCategories,
|
||||
unreleased,
|
||||
};
|
@ -1,84 +0,0 @@
|
||||
const Changelog = require('./changelog');
|
||||
const { unreleased } = require('./constants');
|
||||
|
||||
function truncated(line) {
|
||||
return line.length > 80 ? `${line.slice(0, 80)}...` : line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Changelog instance that represents the given changelog, which
|
||||
* is parsed for release and change informatino.
|
||||
* @param {Object} options
|
||||
* @param {string} options.changelogContent - The changelog to parse
|
||||
* @param {string} options.repoUrl - The GitHub repository URL for the current
|
||||
* project.
|
||||
* @returns {Changelog} A changelog instance that reflects the changelog text
|
||||
* provided.
|
||||
*/
|
||||
function parseChangelog({ changelogContent, repoUrl }) {
|
||||
const changelogLines = changelogContent.split('\n');
|
||||
const changelog = new Changelog({ repoUrl });
|
||||
|
||||
const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`);
|
||||
if (unreleasedHeaderIndex === -1) {
|
||||
throw new Error(`Failed to find ${unreleased} header`);
|
||||
}
|
||||
const unreleasedLinkReferenceDefinition = changelogLines.findIndex((line) => {
|
||||
return line.startsWith(`[${unreleased}]: `);
|
||||
});
|
||||
if (unreleasedLinkReferenceDefinition === -1) {
|
||||
throw new Error(`Failed to find ${unreleased} link reference definition`);
|
||||
}
|
||||
|
||||
const contentfulChangelogLines = changelogLines
|
||||
.slice(unreleasedHeaderIndex + 1, unreleasedLinkReferenceDefinition)
|
||||
.filter((line) => line !== '');
|
||||
|
||||
let mostRecentRelease;
|
||||
let mostRecentCategory;
|
||||
for (const line of contentfulChangelogLines) {
|
||||
if (line.startsWith('## [')) {
|
||||
const results = line.match(
|
||||
/^## \[(\d+\.\d+\.\d+)\](?: - (\d\d\d\d-\d\d-\d\d))?(?: \[(\w+)\])?/u,
|
||||
);
|
||||
if (results === null) {
|
||||
throw new Error(`Malformed release header: '${truncated(line)}'`);
|
||||
}
|
||||
mostRecentRelease = results[1];
|
||||
mostRecentCategory = undefined;
|
||||
const date = results[2];
|
||||
const status = results[3];
|
||||
changelog.addRelease({
|
||||
addToStart: false,
|
||||
date,
|
||||
status,
|
||||
version: mostRecentRelease,
|
||||
});
|
||||
} else if (line.startsWith('### ')) {
|
||||
const results = line.match(/^### (\w+)$\b/u);
|
||||
if (results === null) {
|
||||
throw new Error(`Malformed category header: '${truncated(line)}'`);
|
||||
}
|
||||
mostRecentCategory = results[1];
|
||||
} else if (line.startsWith('- ')) {
|
||||
if (mostRecentCategory === undefined) {
|
||||
throw new Error(`Category missing for change: '${truncated(line)}'`);
|
||||
}
|
||||
const description = line.slice(2);
|
||||
changelog.addChange({
|
||||
addToStart: false,
|
||||
category: mostRecentCategory,
|
||||
description,
|
||||
version: mostRecentRelease,
|
||||
});
|
||||
} else if (mostRecentRelease === null) {
|
||||
continue;
|
||||
} else {
|
||||
throw new Error(`Unrecognized line: '${truncated(line)}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return changelog;
|
||||
}
|
||||
|
||||
module.exports = { parseChangelog };
|
@ -1,171 +0,0 @@
|
||||
const assert = require('assert').strict;
|
||||
const runCommand = require('../runCommand');
|
||||
const { parseChangelog } = require('./parseChangelog');
|
||||
const { changeCategories } = require('./constants');
|
||||
|
||||
async function getMostRecentTag() {
|
||||
const [mostRecentTagCommitHash] = await runCommand('git', [
|
||||
'rev-list',
|
||||
'--tags',
|
||||
'--max-count=1',
|
||||
]);
|
||||
const [mostRecentTag] = await runCommand('git', [
|
||||
'describe',
|
||||
'--tags',
|
||||
mostRecentTagCommitHash,
|
||||
]);
|
||||
assert.equal(mostRecentTag[0], 'v', 'Most recent tag should start with v');
|
||||
return mostRecentTag;
|
||||
}
|
||||
|
||||
async function getCommits(commitHashes) {
|
||||
const commits = [];
|
||||
for (const commitHash of commitHashes) {
|
||||
const [subject] = await runCommand('git', [
|
||||
'show',
|
||||
'-s',
|
||||
'--format=%s',
|
||||
commitHash,
|
||||
]);
|
||||
|
||||
let prNumber;
|
||||
let description = subject;
|
||||
|
||||
// Squash & Merge: the commit subject is parsed as `<description> (#<PR ID>)`
|
||||
if (subject.match(/\(#\d+\)/u)) {
|
||||
const matchResults = subject.match(/\(#(\d+)\)/u);
|
||||
prNumber = matchResults[1];
|
||||
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 matchResults = subject.match(/#(\d+)\sfrom/u);
|
||||
prNumber = matchResults[1];
|
||||
const [firstLineOfBody] = await runCommand('git', [
|
||||
'show',
|
||||
'-s',
|
||||
'--format=%b',
|
||||
commitHash,
|
||||
]);
|
||||
description = firstLineOfBody || subject;
|
||||
}
|
||||
// Otherwise:
|
||||
// Normal commits: The commit subject is the description, and the PR ID is omitted.
|
||||
|
||||
commits.push({ prNumber, description });
|
||||
}
|
||||
return commits;
|
||||
}
|
||||
|
||||
function getAllChangeDescriptions(changelog) {
|
||||
const releases = changelog.getReleases();
|
||||
const changeDescriptions = Object.values(
|
||||
changelog.getUnreleasedChanges(),
|
||||
).flat();
|
||||
for (const release of releases) {
|
||||
changeDescriptions.push(
|
||||
...Object.values(changelog.getReleaseChanges(release.version)).flat(),
|
||||
);
|
||||
}
|
||||
return changeDescriptions;
|
||||
}
|
||||
|
||||
function getAllLoggedPrNumbers(changelog) {
|
||||
const changeDescriptions = getAllChangeDescriptions(changelog);
|
||||
|
||||
const prNumbersWithChangelogEntries = [];
|
||||
for (const description of changeDescriptions) {
|
||||
const matchResults = description.match(/^\[#(\d+)\]/u);
|
||||
if (matchResults === null) {
|
||||
continue;
|
||||
}
|
||||
const prNumber = matchResults[1];
|
||||
prNumbersWithChangelogEntries.push(prNumber);
|
||||
}
|
||||
|
||||
return prNumbersWithChangelogEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./constants.js').Version} Version
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update a changelog with any commits made since the last release. Commits for
|
||||
* PRs that are already included in the changelog are omitted.
|
||||
* @param {Object} options
|
||||
* @param {string} options.changelogContent - The current changelog
|
||||
* @param {Version} options.currentVersion - The current version
|
||||
* @param {string} options.repoUrl - The GitHub repository URL for the current
|
||||
* project.
|
||||
* @param {boolean} options.isReleaseCandidate - Denotes whether the current
|
||||
* project is in the midst of release preparation or not. If this is set, any
|
||||
* new changes are listed under the current release header. Otherwise, they
|
||||
* are listed under the 'Unreleased' section.
|
||||
* @returns
|
||||
*/
|
||||
async function updateChangelog({
|
||||
changelogContent,
|
||||
currentVersion,
|
||||
repoUrl,
|
||||
isReleaseCandidate,
|
||||
}) {
|
||||
const changelog = parseChangelog({ changelogContent, repoUrl });
|
||||
|
||||
// Ensure we have all tags on remote
|
||||
await runCommand('git', ['fetch', '--tags']);
|
||||
const mostRecentTag = await getMostRecentTag();
|
||||
const commitsHashesSinceLastRelease = await runCommand('git', [
|
||||
'rev-list',
|
||||
`${mostRecentTag}..HEAD`,
|
||||
]);
|
||||
const commits = await getCommits(commitsHashesSinceLastRelease);
|
||||
|
||||
const loggedPrNumbers = getAllLoggedPrNumbers(changelog);
|
||||
const newCommits = commits.filter(
|
||||
({ prNumber }) => !loggedPrNumbers.includes(prNumber),
|
||||
);
|
||||
|
||||
const hasUnreleasedChanges = changelog.getUnreleasedChanges().length !== 0;
|
||||
if (
|
||||
newCommits.length === 0 &&
|
||||
(!isReleaseCandidate || hasUnreleasedChanges)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Ensure release header exists, if necessary
|
||||
if (
|
||||
isReleaseCandidate &&
|
||||
!changelog
|
||||
.getReleases()
|
||||
.find((release) => release.version === currentVersion)
|
||||
) {
|
||||
changelog.addRelease({ version: currentVersion });
|
||||
}
|
||||
|
||||
if (isReleaseCandidate && hasUnreleasedChanges) {
|
||||
changelog.migrateUnreleasedChangesToRelease(currentVersion);
|
||||
}
|
||||
|
||||
const newChangeEntries = newCommits.map(({ prNumber, description }) => {
|
||||
if (prNumber) {
|
||||
const prefix = `[#${prNumber}](${repoUrl}/pull/${prNumber})`;
|
||||
return `${prefix}: ${description}`;
|
||||
}
|
||||
return description;
|
||||
});
|
||||
|
||||
for (const description of newChangeEntries.reverse()) {
|
||||
changelog.addChange({
|
||||
version: isReleaseCandidate ? currentVersion : undefined,
|
||||
category: changeCategories.Uncategorized,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return changelog.toString();
|
||||
}
|
||||
|
||||
module.exports = { updateChangelog };
|
@ -1,79 +0,0 @@
|
||||
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;
|
@ -2,6 +2,10 @@
|
||||
"name": "metamask-crx",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MetaMask/metamask-extension"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "yarn install && yarn setup:postinstall",
|
||||
"setup:postinstall": "yarn patch-package && yarn allow-scripts",
|
||||
@ -57,7 +61,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": "node ./development/auto-changelog.js",
|
||||
"update-changelog": "auto-changelog update",
|
||||
"generate:migration": "./development/generate-migration.sh",
|
||||
"lavamoat:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
|
||||
"lavamoat:debug": "lavamoat ./development/build/index.js --writeAutoPolicyDebug"
|
||||
@ -205,6 +209,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.5.5",
|
||||
"@lavamoat/allow-scripts": "^1.0.4",
|
||||
"@metamask/auto-changelog": "^1.0.0",
|
||||
"@metamask/eslint-config": "^6.0.0",
|
||||
"@metamask/eslint-config-jest": "^6.0.0",
|
||||
"@metamask/eslint-config-mocha": "^6.0.0",
|
||||
|
28
yarn.lock
28
yarn.lock
@ -2619,6 +2619,16 @@
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0"
|
||||
|
||||
"@metamask/auto-changelog@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/auto-changelog/-/auto-changelog-1.0.0.tgz#ca6a71d1b983cf08b715bdcd8e240d746974d0c7"
|
||||
integrity sha512-3Bcm+JsEmNllPi7kRtzS6EAjYTzz+Isa4QFq2DQ4DFwIsv2HUxdR+KNU2GJ1BdX4lbPcQTrpTdaPgBZ9G4NhLA==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
diff "^5.0.0"
|
||||
semver "^7.3.5"
|
||||
yargs "^17.0.1"
|
||||
|
||||
"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.22.0", "@metamask/contract-metadata@^1.23.0":
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.25.0.tgz#442ace91fb40165310764b68d8096d0017bb0492"
|
||||
@ -9185,6 +9195,11 @@ diff@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
diff@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
|
||||
@ -27561,6 +27576,19 @@ yargs@^16.0.0, yargs@^16.2.0:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^17.0.1:
|
||||
version "17.0.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
|
||||
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^7.1.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"
|
||||
|
Loading…
Reference in New Issue
Block a user