@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
224 lines • 9.29 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const os = require("os");
const command_1 = require("@salesforce/command");
const ts_types_1 = require("@salesforce/ts-types");
const kit_1 = require("@salesforce/kit");
const core_1 = require("@octokit/core");
const chalk_1 = require("chalk");
const core_2 = require("@salesforce/core");
const shelljs_1 = require("shelljs");
const semver = require("semver");
const types_1 = require("../../types");
const package_1 = require("../../package");
core_2.Messages.importMessagesDirectory(__dirname);
const messages = core_2.Messages.loadMessages('@salesforce/plugin-release-management', 'cli.releasenotes');
function isNotEmpty(obj) {
return Object.keys(obj).length > 0;
}
class ReleaseNotes extends command_1.SfdxCommand {
constructor() {
super(...arguments);
this.usernames = {};
}
async run() {
const auth = (0, ts_types_1.ensureString)(new kit_1.Env().getString('GH_TOKEN'), 'GH_TOKEN is required to be set in the environment');
this.octokit = new core_1.Octokit({ auth });
const cli = (0, ts_types_1.ensure)(this.flags.cli);
const fullName = cli === types_1.CLI.SF ? '@salesforce/cli' : 'sfdx-cli';
const npmPackage = this.getNpmPackage(fullName, this.flags.since ?? 'latest');
const latestrc = this.getNpmPackage(fullName, 'latest-rc');
const oldPlugins = this.normalizePlugins(npmPackage);
const newPlugins = this.normalizePlugins(latestrc);
const differences = this.findDifferences(oldPlugins, newPlugins);
if (isNotEmpty(differences.upgraded)) {
this.ux.styledHeader('Upgraded Plugins');
for (const [plugin, version] of Object.entries(differences.upgraded)) {
this.ux.log(`• ${plugin} ${oldPlugins[plugin]} => ${version}`);
}
}
if (isNotEmpty(differences.downgraded)) {
this.ux.styledHeader('Downgraded Plugins');
for (const [plugin, version] of Object.entries(differences.downgraded)) {
this.ux.log(`• ${plugin} ${oldPlugins[plugin]} => ${version}`);
}
}
if (isNotEmpty(differences.added)) {
this.ux.styledHeader('Added Plugins');
for (const [plugin, version] of Object.entries(differences.added)) {
this.ux.log(`• ${plugin} ${version}`);
}
}
if (isNotEmpty(differences.removed)) {
this.ux.styledHeader('Removed Plugins');
for (const [plugin, version] of Object.entries(differences.removed)) {
this.ux.log(`• ${plugin} ${version}`);
}
}
const changesByPlugin = {};
for (const [plugin] of Object.entries(differences.upgraded)) {
const pkg = this.getNpmPackage(plugin, oldPlugins[plugin]);
const publishDate = pkg.time[pkg.version];
const changes = await this.getPullsForPlugin(plugin, publishDate);
if (changes.length)
changesByPlugin[plugin] = changes;
}
if (this.flags.markdown) {
this.logChangesMarkdown(changesByPlugin);
}
else {
this.logChanges(changesByPlugin);
}
return changesByPlugin;
}
getNpmPackage(name, version = 'latest') {
const result = (0, shelljs_1.exec)(`npm view ${name}@${version} --json`, { silent: true });
return JSON.parse(result.stdout);
}
normalizePlugins(npmPackage) {
const plugins = npmPackage.oclif?.plugins ?? [];
const normalized = { [npmPackage.name]: npmPackage.version };
plugins.forEach((p) => {
const version = (0, package_1.parsePackageVersion)(npmPackage.dependencies[p]);
if (npmPackage.dependencies[p].startsWith('npm:')) {
const name = (0, package_1.parseAliasedPackageName)(npmPackage.dependencies[p]);
normalized[name] = version;
}
else {
normalized[p] = version;
}
});
return normalized;
}
findDifferences(oldPlugins, newPlugins) {
const removed = {};
const added = {};
const upgraded = {};
const downgraded = {};
const unchanged = {};
for (const [name, version] of Object.entries(oldPlugins)) {
if (!newPlugins[name])
removed[name] = version;
}
for (const [name, version] of Object.entries(newPlugins)) {
if (!oldPlugins[name])
added[name] = version;
else if (semver.gt(version, oldPlugins[name]))
upgraded[name] = version;
else if (semver.lt(version, oldPlugins[name]))
downgraded[name] = version;
else
unchanged[name] = version;
}
return { removed, added, upgraded, downgraded, unchanged };
}
async getNameOfUser(username) {
if (this.usernames[username])
return this.usernames[username];
const { data } = await this.octokit.request('GET /users/{username}', { username });
const name = (data.name ?? data.login ?? username);
this.usernames[username] = name;
return name;
}
async getPullsForPlugin(plugin, publishDate) {
const npmPackage = this.getNpmPackage(plugin);
const homepage = npmPackage.homepage ?? (npmPackage.name === 'salesforce-alm' ? 'salesforcecli/toolbelt' : null);
if (!homepage) {
throw new core_2.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) => {
return pr.merged_at && pr.merged_at > publishDate && !pr.user.login.includes('dependabot');
})
.map(async (pr) => {
const username = await this.getNameOfUser(pr.user.login);
const author = pr.user.login === username ? username : `${username} (${pr.user.login})`;
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.ux.styledHeader((0, chalk_1.cyan)(plugin));
for (const change of changes) {
this.log((0, chalk_1.bold)(`${change.title}`));
for (const [key, value] of Object.entries(change)) {
if (['title', 'plugin'].includes(key))
continue;
if (key === 'description') {
this.log(`${key}:\n${(0, chalk_1.dim)(value)}`);
}
else {
this.log(`${key}: ${(0, chalk_1.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();
}
}
}
exports.default = ReleaseNotes;
ReleaseNotes.description = messages.getMessage('description');
ReleaseNotes.examples = messages.getMessage('examples').split(os.EOL);
ReleaseNotes.flagsConfig = {
cli: command_1.flags.string({
description: messages.getMessage('cliFlag'),
options: Object.values(types_1.CLI),
char: 'c',
required: true,
}),
since: command_1.flags.string({
description: messages.getMessage('sinceFlag'),
char: 's',
}),
markdown: command_1.flags.boolean({
description: messages.getMessage('markdownFlag'),
char: 'm',
default: false,
}),
};
//# sourceMappingURL=releasenotes.js.map