@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
221 lines • 9.42 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { ensure, ensureString } from '@salesforce/ts-types';
import { Env } from '@salesforce/kit';
import { Octokit } from '@octokit/core';
import chalk from 'chalk';
import { Messages, SfError } from '@salesforce/core';
import shelljs from 'shelljs';
import semver from 'semver';
import { CLI } from '../../types.js';
import { parsePackageVersion } from '../../package.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.releasenotes');
export default class ReleaseNotes extends SfCommand {
static summary = messages.getMessage('description');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static flags = {
cli: Flags.string({
summary: messages.getMessage('flags.cli.summary'),
options: Object.values(CLI),
char: 'c',
required: true,
}),
since: Flags.string({
summary: messages.getMessage('flags.since.summary'),
char: 's',
}),
markdown: Flags.boolean({
summary: messages.getMessage('flags.markdown.summary'),
char: 'm',
default: false,
}),
};
octokit;
usernames = new Map();
async run() {
const { flags } = await this.parse(ReleaseNotes);
const auth = ensureString(new Env().getString('GH_TOKEN'), 'GH_TOKEN is required to be set in the environment');
this.octokit = new Octokit({ auth });
const cli = ensure(flags.cli);
const fullName = cli === CLI.SF ? '@salesforce/cli' : 'sfdx-cli';
const npmPackage = getNpmPackage(fullName, flags.since ?? 'latest');
const latestrc = getNpmPackage(fullName, 'latest-rc');
const oldPlugins = normalizePlugins(npmPackage);
const newPlugins = normalizePlugins(latestrc);
const differences = findDifferences(oldPlugins, newPlugins);
if (differences.upgraded.size) {
this.styledHeader('Upgraded Plugins');
for (const [plugin, version] of differences.upgraded.entries()) {
this.log(`• ${plugin} ${oldPlugins.get(plugin) ?? '<no match in old plugins>'} => ${version}`);
}
}
if (differences.downgraded.size) {
this.styledHeader('Downgraded Plugins');
for (const [plugin, version] of differences.downgraded.entries()) {
this.log(`• ${plugin} ${oldPlugins.get(plugin) ?? '<no match in old plugins>'} => ${version}`);
}
}
if (differences.added.size) {
this.styledHeader('Added Plugins');
for (const [plugin, version] of differences.added.entries()) {
this.log(`• ${plugin} ${version}`);
}
}
if (differences.removed.size) {
this.styledHeader('Removed Plugins');
for (const [plugin, version] of differences.removed.entries()) {
this.log(`• ${plugin} ${version}`);
}
}
const changesByPlugin = {};
for (const plugin of differences.upgraded.keys()) {
const pkg = getNpmPackage(plugin, oldPlugins.get(plugin));
const publishDate = pkg.time?.[pkg.version];
// eslint-disable-next-line no-await-in-loop
const changes = await this.getPullsForPlugin(plugin, publishDate);
if (changes.length)
changesByPlugin[plugin] = changes;
}
if (flags.markdown) {
this.logChangesMarkdown(changesByPlugin);
}
else {
this.logChanges(changesByPlugin);
}
return changesByPlugin;
}
async getNameOfUser(username) {
const value = this.usernames.get(username);
if (value)
return value;
const { data } = await this.octokit.request('GET /users/{username}', { username });
const name = data.name ?? data.login ?? username;
this.usernames.set(username, name);
return name;
}
async getPullsForPlugin(plugin, publishDate) {
const npmPackage = getNpmPackage(plugin);
const homepage = npmPackage.homepage ?? (npmPackage.name === 'salesforce-alm' ? 'salesforcecli/toolbelt' : null);
if (!homepage) {
throw new SfError(`No github url found for ${npmPackage.name}`, 'GitUrlNotFound');
}
const [owner, repo] = homepage.replace('https://github.com/', '').replace(/#(.*)/g, '').split('/');
const pullRequests = await this.octokit.request('GET /repos/{owner}/{repo}/pulls', {
owner,
repo,
state: 'closed',
base: 'main',
// eslint-disable-next-line camelcase
per_page: 100,
});
const changes = (await Promise.all(pullRequests.data
.filter((pr) => pr.merged_at && (!publishDate || pr.merged_at > publishDate) && !pr.user?.login.includes('dependabot'))
.map(async (pr) => {
const prUserLogin = ensureString(pr.user?.login, `No user.login property found for ${JSON.stringify(pr)}`);
const username = await this.getNameOfUser(prUserLogin);
const author = pr.user?.login === username ? username : `${username} (${prUserLogin})`;
return {
author,
mergedAt: pr.merged_at,
mergedInto: pr.base.ref,
link: pr.html_url,
title: pr.title,
description: (pr.body ?? '').trim(),
plugin,
};
})));
return changes;
}
logChanges(changesByPlugin) {
for (const [plugin, changes] of Object.entries(changesByPlugin)) {
this.styledHeader(chalk.cyan(plugin));
for (const change of changes) {
this.log(chalk.bold(`${change.title}`));
for (const [key, value] of Object.entries(change)) {
if (['title', 'plugin'].includes(key))
continue;
if (key === 'description') {
this.log(`${key}:\n${chalk.dim(value)}`);
}
else {
this.log(`${key}: ${chalk.dim(value)}`);
}
}
this.log();
}
this.log();
}
}
logChangesMarkdown(changesByPlugin) {
for (const [plugin, changes] of Object.entries(changesByPlugin)) {
this.log(`## ${plugin}`);
for (const change of changes) {
this.log(`\n### ${change.title}`);
for (const [key, value] of Object.entries(change)) {
if (['title', 'plugin'].includes(key))
continue;
if (key === 'description') {
this.log(`- ${key}:\n\`\`\`\n${value}\n\`\`\``);
}
else {
this.log(`- ${key}: ${value}`);
}
}
this.log();
}
this.log();
}
}
}
const getNpmPackage = (name, version = 'latest') => {
const result = shelljs.exec(`npm view ${name}@${version} --json`, { silent: true });
return JSON.parse(result.stdout);
};
const normalizePlugins = (npmPackage) => {
const plugins = npmPackage.oclif?.plugins ?? [];
const dependencies = npmPackage.dependencies ?? {};
// return normalized;
const pluginsTuples = plugins.map((p) => {
const version = parsePackageVersion(dependencies[p]);
if (!version) {
throw new SfError(`Could not find version for ${p}`, 'VersionNotFound');
}
return [p, version];
});
return new Map([[npmPackage.name, npmPackage.version], ...pluginsTuples]);
};
const findDifferences = (oldPlugins, newPlugins) => {
const removed = new Map();
const added = new Map();
const upgraded = new Map();
const downgraded = new Map();
const unchanged = new Map();
// if it's in the old, but not in the new
oldPlugins.forEach((version, name) => {
if (!newPlugins.has(name))
removed.set(name, version);
});
newPlugins.forEach((version, name) => {
// these are in the new, but not in the old
if (!oldPlugins.has(name))
added.set(name, version);
// non-null because they aren't added (new, but not old, so we know that must be in the old)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
else if (semver.gt(version, oldPlugins.get(name)))
upgraded.set(name, version);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
else if (semver.lt(version, oldPlugins.get(name)))
downgraded.set(name, version);
else
unchanged.set(name, version);
});
return { removed, added, upgraded, downgraded, unchanged };
};
//# sourceMappingURL=releasenotes.js.map