mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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",
|
"name": "metamask-crx",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MetaMask/metamask-extension"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "yarn install && yarn setup:postinstall",
|
"setup": "yarn install && yarn setup:postinstall",
|
||||||
"setup:postinstall": "yarn patch-package && yarn allow-scripts",
|
"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": "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: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",
|
"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",
|
"generate:migration": "./development/generate-migration.sh",
|
||||||
"lavamoat:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
|
"lavamoat:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
|
||||||
"lavamoat:debug": "lavamoat ./development/build/index.js --writeAutoPolicyDebug"
|
"lavamoat:debug": "lavamoat ./development/build/index.js --writeAutoPolicyDebug"
|
||||||
@ -205,6 +209,7 @@
|
|||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/register": "^7.5.5",
|
"@babel/register": "^7.5.5",
|
||||||
"@lavamoat/allow-scripts": "^1.0.4",
|
"@lavamoat/allow-scripts": "^1.0.4",
|
||||||
|
"@metamask/auto-changelog": "^1.0.0",
|
||||||
"@metamask/eslint-config": "^6.0.0",
|
"@metamask/eslint-config": "^6.0.0",
|
||||||
"@metamask/eslint-config-jest": "^6.0.0",
|
"@metamask/eslint-config-jest": "^6.0.0",
|
||||||
"@metamask/eslint-config-mocha": "^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"
|
prop-types "^15.7.2"
|
||||||
react-is "^16.8.0"
|
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":
|
"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.22.0", "@metamask/contract-metadata@^1.23.0":
|
||||||
version "1.25.0"
|
version "1.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.25.0.tgz#442ace91fb40165310764b68d8096d0017bb0492"
|
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"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
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:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
|
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"
|
y18n "^5.0.5"
|
||||||
yargs-parser "^20.2.2"
|
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:
|
yargs@^7.1.0:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"
|
||||||
|
Loading…
Reference in New Issue
Block a user