UNPKG

@salesforce/plugin-release-management

Version:
138 lines 5.94 kB
/* * 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