@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
138 lines • 5.94 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
*/
/* eslint-disable camelcase*/
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { Octokit } from '@octokit/core';
import { Env } from '@salesforce/kit';
import { ensureString } from '@salesforce/ts-types';
import { Messages, SfError } from '@salesforce/core';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.release.automerge');
/**
* Bot accounts whose PRs are eligible for automerge. Add to this list (via
* reviewed PR) to onboard a new bot.
*/
export const ALLOWED_BOT_USERS = ['svc-cli-bot', 'svc-idee-bot'];
export function isAllowedBotUser(login) {
return !!login && ALLOWED_BOT_USERS.includes(login);
}
function getGitHubToken() {
const env = new Env();
return ensureString(env.getString('GH_TOKEN') ?? env.getString('GITHUB_TOKEN'), 'GH_TOKEN or GITHUB_TOKEN is required to be set in the environment.');
}
export default class AutoMerge extends SfCommand {
static summary = messages.getMessage('description');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static flags = {
owner: Flags.string({
summary: messages.getMessage('flags.owner.summary'),
dependsOn: ['repo'],
aliases: ['org'],
required: true,
}),
repo: Flags.string({
summary: messages.getMessage('flags.repo.summary'),
dependsOn: ['owner'],
required: true,
}),
'pull-number': Flags.integer({
summary: messages.getMessage('flags.pull-number.summary'),
required: true,
}),
'dry-run': Flags.boolean({
summary: messages.getMessage('flags.dry-run.summary'),
char: 'd',
default: false,
}),
verbose: Flags.boolean({
summary: messages.getMessage('flags.verbose.summary'),
default: false,
}),
};
octokit = new Octokit({ auth: getGitHubToken() });
// 2 props set early in run method
baseRepoParams;
pullRequestParams;
async run() {
const { flags } = await this.parse(AutoMerge);
const { 'dry-run': dryRun, owner, repo, verbose, 'pull-number': pullNumber } = flags;
this.baseRepoParams = { owner, repo };
this.pullRequestParams = {
...this.baseRepoParams,
pull_number: pullNumber,
};
const prData = (await this.octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { ...this.pullRequestParams })).data;
// Check if PR is able to be merged.
const stop = (reason) => {
if (verbose)
this.styledJSON(prData);
throw new SfError(`CANNOT MERGE: ${reason}`, 'AUTOMERGE_FAILURE', [
'Run with --verbose to see PR response object',
'Also try running this locally with the "--dry-run" flag',
]);
};
if (prData.state !== 'open') {
stop('PR not open');
}
const automergeLabels = ['automerge', 'nightly-automerge'];
if (!prData.labels.some((label) => label.name && automergeLabels.includes(label.name))) {
stop(`Missing automerge label: [${automergeLabels.join(', ')}]`);
}
if (!isAllowedBotUser(prData.user?.login)) {
stop(`PR must be created by one of: [${ALLOWED_BOT_USERS.join(', ')}]`);
}
if (!(await this.isGreen(prData, verbose))) {
stop('PR checks failed');
}
if (!(await this.isMergeable())) {
stop('PR is not mergable');
}
// Continue with merge attempt
if (dryRun === false) {
this.log(`Merging ${prData.number} | ${prData.title}`);
const mergeResult = await this.octokit.request('PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge', {
...this.pullRequestParams,
});
this.info('Run with --verbose to see PR merge response');
if (verbose) {
this.styledJSON(mergeResult);
}
}
else {
this.logSuccess(`Dry run successful: ${prData.number} | ${prData.title}`);
}
}
async isGreen(pr, verbose) {
const statusResponse = await this.octokit.request('GET /repos/{owner}/{repo}/commits/{ref}/status', {
...this.baseRepoParams,
ref: pr.head.sha,
});
// no point looking at check runs if the commit status is not green
if (statusResponse.data.state !== 'success') {
return false;
}
const checkRunResponse = await this.octokit.request('GET /repos/{owner}/{repo}/commits/{ref}/check-runs', {
...this.baseRepoParams,
ref: pr.head.sha,
per_page: 50,
});
if (verbose)
this.styledJSON(checkRunResponse);
return checkRunResponse.data.check_runs.every((cr) => cr.name === 'automerge' ||
(cr.status === 'completed' && cr.conclusion && ['success', 'skipped'].includes(cr.conclusion)));
}
async isMergeable() {
const statusResponse = await this.octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
...this.pullRequestParams,
});
// mergeable_state of 'blocked' is ok because it is either missing an approval or the commit is not signed.
// We're screening out 'behind' which might be merge conflicts.
return statusResponse.data.mergeable === true && statusResponse.data.mergeable_state !== 'behind';
}
}
//# sourceMappingURL=automerge.js.map