From 3b12066d9430b6dac57a299f687ae431d237b6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Sun, 21 Jun 2020 15:23:51 +0200 Subject: [PATCH] Add options: `auto-assign` and `assignees` (#205) --- README.md | 2 + docs/configuration.md | 28 +++++++++++ src/options/cliArgs.test.ts | 10 ++-- src/options/cliArgs.ts | 18 +++++++- src/options/options.test.ts | 2 + src/runWithOptions.test.ts | 1 + .../v3/addAssigneesToPullRequest.test.ts | 35 ++++++++++++++ .../github/v3/addAssigneesToPullRequest.ts | 46 +++++++++++++++++++ src/services/github/v3/createPullRequest.ts | 2 +- .../__snapshots__/integration.test.ts.snap | 1 + src/ui/cherrypickAndCreatePullRequest.test.ts | 3 ++ .../cherrypickAndCreateTargetPullRequest.ts | 22 ++++++--- 12 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 src/services/github/v3/addAssigneesToPullRequest.test.ts create mode 100644 src/services/github/v3/addAssigneesToPullRequest.ts diff --git a/README.md b/README.md index 4823f9b5..cf7bdbb4 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ See [configuration.md](https://github.com/sqren/backport/blob/master/docs/config | --accesstoken | Github access token | | string | | --all | Show commits from other than me | false | boolean | | --author | Filter commits by author | _Current user_ | string | +| --assignees | Assign users to target pull request | | string | +| --auto-assign | Assign current user to target pull request | false | boolean | | --branch | Target branch to backport to | | string | | --dry-run | Perform backport without pushing to Github | false | boolean | | --editor | Editor (eg. `code`) to open and solve conflicts | | string | diff --git a/docs/configuration.md b/docs/configuration.md index 9edd760d..ddbdec72 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -119,6 +119,34 @@ Default: `false` CLI: `--all`, `-a` +#### `assignees` + +Add assignees to the target pull request + +CLI: `--assignees `, `-assign ` + +Config: + +```json +{ + "assignees": ["sqren"] +} +``` + +#### `autoAssign` + +Automatically add the current user as assignee to the target pull request + +CLI: `--auto-assign` + +Config: + +```json +{ + "autoAssign": true +} +``` + #### `branchLabelMapping` Pre-select target branch choices based on the source PR labels. diff --git a/src/options/cliArgs.test.ts b/src/options/cliArgs.test.ts index 7f2f5cc0..99180f41 100644 --- a/src/options/cliArgs.test.ts +++ b/src/options/cliArgs.test.ts @@ -1,9 +1,8 @@ -import { getOptionsFromCliArgs } from './cliArgs'; -import { OptionsFromConfigFiles } from './config/config'; +import { getOptionsFromCliArgs, OptionsFromCliArgs } from './cliArgs'; describe('getOptionsFromCliArgs', () => { it('should return correct options', () => { - const configOptions = { + const configOptions: Partial = { accessToken: 'myAccessToken', all: false, fork: true, @@ -39,6 +38,7 @@ describe('getOptionsFromCliArgs', () => { expect(res).toEqual({ accessToken: 'myAccessToken', all: true, + assignees: [], dryRun: false, fork: true, gitHostname: 'github.com', @@ -62,7 +62,7 @@ describe('getOptionsFromCliArgs', () => { }); it('should accept both camel-case and dashed-case and convert them to camel cased', () => { - const configOptions = {} as OptionsFromConfigFiles; + const configOptions: Partial = {}; const argv = [ '--access-token', 'my access token', @@ -79,7 +79,7 @@ describe('getOptionsFromCliArgs', () => { }); it('should accept aliases (--pr) but only return the full name (--pullNumber) in the result', () => { - const configOptions = {} as OptionsFromConfigFiles; + const configOptions: Partial = {}; const argv = ['--pr', '1337']; const res = getOptionsFromCliArgs(configOptions, argv); diff --git a/src/options/cliArgs.ts b/src/options/cliArgs.ts index bfccab04..3f413d82 100644 --- a/src/options/cliArgs.ts +++ b/src/options/cliArgs.ts @@ -45,6 +45,19 @@ export function getOptionsFromCliArgs( type: 'string', }) + .option('assignees', { + default: (configOptions.assignees || []) as string[], + description: 'Add assignees to the target pull request', + alias: ['assignee', 'assign'], + type: 'array', + }) + + .option('autoAssign', { + default: (configOptions.autoAssign ?? false) as boolean, + description: 'Auto assign the target pull request to yourself', + type: 'boolean', + }) + .option('dryRun', { default: false, description: 'Perform backport without pushing to Github', @@ -266,11 +279,14 @@ export function getOptionsFromCliArgs( ).argv; // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - const { $0, _, verify, multiple, ...rest } = cliArgs; + const { $0, _, verify, multiple, autoAssign, ...rest } = cliArgs; return { ...rest, + // auto-assign the current user to the target pull request or the assignees specified + assignees: autoAssign ? [rest.username as string] : rest.assignees, + // `branchLabelMapping` is not available as cli argument branchLabelMapping: configOptions.branchLabelMapping as BranchLabelMapping, diff --git a/src/options/options.test.ts b/src/options/options.test.ts index ccb9844b..4144b367 100644 --- a/src/options/options.test.ts +++ b/src/options/options.test.ts @@ -75,6 +75,7 @@ describe('getOptions', () => { accessToken: 'myAccessToken', all: false, author: 'sqren', + assignees: [], dryRun: false, fork: true, gitHostname: 'github.com', @@ -107,6 +108,7 @@ describe('validateRequiredOptions', () => { accessToken: 'myAccessToken', all: false, author: undefined, + assignees: [], branchLabelMapping: undefined, dryRun: false, editor: 'code', diff --git a/src/runWithOptions.test.ts b/src/runWithOptions.test.ts index 642f2a5a..6c8badf2 100644 --- a/src/runWithOptions.test.ts +++ b/src/runWithOptions.test.ts @@ -23,6 +23,7 @@ describe('runWithOptions', () => { accessToken: 'myAccessToken', all: false, author: 'sqren', + assignees: [], branchLabelMapping: undefined, dryRun: false, editor: 'code', diff --git a/src/services/github/v3/addAssigneesToPullRequest.test.ts b/src/services/github/v3/addAssigneesToPullRequest.test.ts new file mode 100644 index 00000000..f03431ca --- /dev/null +++ b/src/services/github/v3/addAssigneesToPullRequest.test.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { BackportOptions } from '../../../options/options'; +import { addAssigneesToPullRequest } from './addAssigneesToPullRequest'; + +describe('addAssigneesToPullRequest', () => { + it('should add assignees to PR', async () => { + const pullNumber = 216; + const assignees = ['sqren']; + + const spy = jest + .spyOn(axios, 'request') + .mockResolvedValueOnce('some-response'); + + await addAssigneesToPullRequest( + { + githubApiBaseUrlV3: 'https://api.github.com', + repoName: 'backport-demo', + repoOwner: 'sqren', + accessToken: 'my-token', + username: 'sqren', + dryRun: false, + } as BackportOptions, + pullNumber, + assignees + ); + + expect(spy).toHaveBeenCalledWith({ + method: 'post', + url: + 'https://api.github.com/repos/sqren/backport-demo/issues/216/assignees', + auth: { username: 'sqren', password: 'my-token' }, + data: { assignees: ['sqren'] }, + }); + }); +}); diff --git a/src/services/github/v3/addAssigneesToPullRequest.ts b/src/services/github/v3/addAssigneesToPullRequest.ts new file mode 100644 index 00000000..95f3912e --- /dev/null +++ b/src/services/github/v3/addAssigneesToPullRequest.ts @@ -0,0 +1,46 @@ +import ora from 'ora'; +import { BackportOptions } from '../../../options/options'; +import { logger } from '../../logger'; +import { apiRequestV3 } from './apiRequestV3'; + +export async function addAssigneesToPullRequest( + { + githubApiBaseUrlV3, + repoName, + repoOwner, + accessToken, + username, + dryRun, + }: BackportOptions, + pullNumber: number, + assignees: string[] +): Promise { + const isSelfAssigning = assignees.length === 1 && assignees[0] === username; + + const text = isSelfAssigning + ? `Self-assigning to #${pullNumber}` + : `Adding assignees to #${pullNumber}: ${assignees.join(', ')}`; + logger.info(text); + const spinner = ora(text).start(); + + try { + if (dryRun) { + spinner.succeed(`Dry run: ${text}`); + return; + } + + await apiRequestV3({ + method: 'post', + url: `${githubApiBaseUrlV3}/repos/${repoOwner}/${repoName}/issues/${pullNumber}/assignees`, + data: { assignees }, + auth: { + username: username, + password: accessToken, + }, + }); + spinner.succeed(); + } catch (e) { + spinner.fail(); + logger.info(`Could not add assignees to PR ${pullNumber}`, e.stack); + } +} diff --git a/src/services/github/v3/createPullRequest.ts b/src/services/github/v3/createPullRequest.ts index 7bdfd26f..864b53b2 100644 --- a/src/services/github/v3/createPullRequest.ts +++ b/src/services/github/v3/createPullRequest.ts @@ -31,7 +31,7 @@ export async function createPullRequest( const spinner = ora(`Creating pull request`).start(); if (dryRun) { - spinner.succeed(); + spinner.succeed('Dry run: Creating pull request'); return { html_url: 'example_url', number: 1337 }; } diff --git a/src/test/integration/__snapshots__/integration.test.ts.snap b/src/test/integration/__snapshots__/integration.test.ts.snap index 661d8548..c40d6ec1 100644 --- a/src/test/integration/__snapshots__/integration.test.ts.snap +++ b/src/test/integration/__snapshots__/integration.test.ts.snap @@ -176,6 +176,7 @@ Array [ Object { "accessToken": "myAccessToken", "all": false, + "assignees": Array [], "author": "sqren", "branchLabelMapping": undefined, "dryRun": false, diff --git a/src/ui/cherrypickAndCreatePullRequest.test.ts b/src/ui/cherrypickAndCreatePullRequest.test.ts index 72f16d1c..02ba064e 100644 --- a/src/ui/cherrypickAndCreatePullRequest.test.ts +++ b/src/ui/cherrypickAndCreatePullRequest.test.ts @@ -43,6 +43,7 @@ describe('cherrypickAndCreateTargetPullRequest', () => { .mockResolvedValue({ stdout: '', stderr: '' }); const options = { + assignees: [] as string[], githubApiBaseUrlV3: 'https://api.github.com', fork: true, targetPRLabels: ['backport'], @@ -142,6 +143,7 @@ describe('cherrypickAndCreateTargetPullRequest', () => { describe('when commit does not have a pull request reference', () => { beforeEach(async () => { const options = { + assignees: [] as string[], githubApiBaseUrlV3: 'https://api.github.com', fork: true, targetPRLabels: ['backport'], @@ -202,6 +204,7 @@ describe('cherrypickAndCreateTargetPullRequest', () => { const execSpy = setupExecSpy(); const options = { + assignees: [] as string[], fork: true, targetPRLabels: ['backport'], prTitle: '[{targetBranch}] {commitMessages}', diff --git a/src/ui/cherrypickAndCreateTargetPullRequest.ts b/src/ui/cherrypickAndCreateTargetPullRequest.ts index 011cde4f..3f5a66ef 100644 --- a/src/ui/cherrypickAndCreateTargetPullRequest.ts +++ b/src/ui/cherrypickAndCreateTargetPullRequest.ts @@ -17,6 +17,7 @@ import { getFilesWithConflicts, } from '../services/git'; import { getShortSha } from '../services/github/commitFormatters'; +import { addAssigneesToPullRequest } from '../services/github/v3/addAssigneesToPullRequest'; import { addLabelsToPullRequest } from '../services/github/v3/addLabelsToPullRequest'; import { createPullRequest } from '../services/github/v3/createPullRequest'; import { consoleLog } from '../services/logger'; @@ -53,18 +54,27 @@ export async function cherrypickAndCreateTargetPullRequest({ spinner.stop(); const payload = getPullRequestPayload(options, targetBranch, commits); - const pullRequest = await createPullRequest(options, payload); + const targetPullRequest = await createPullRequest(options, payload); - // add targetPRLabels + // add assignees to target pull request + if (options.assignees.length > 0) { + await addAssigneesToPullRequest( + options, + targetPullRequest.number, + options.assignees + ); + } + + // add labels to target pull request if (options.targetPRLabels.length > 0) { await addLabelsToPullRequest( options, - pullRequest.number, + targetPullRequest.number, options.targetPRLabels ); } - // add sourcePRLabels + // add labels to source pull requests if (options.sourcePRLabels.length > 0) { const promises = commits.map((commit) => { if (commit.pullNumber) { @@ -78,7 +88,7 @@ export async function cherrypickAndCreateTargetPullRequest({ await Promise.all(promises); } - consoleLog(`View pull request: ${pullRequest.html_url}`); + consoleLog(`View pull request: ${targetPullRequest.html_url}`); // output PR summary in dry run mode if (options.dryRun) { @@ -88,7 +98,7 @@ export async function cherrypickAndCreateTargetPullRequest({ consoleLog(`Body: ${payload.body}\n`); } - return pullRequest; + return targetPullRequest; } function getFeatureBranchName(targetBranch: string, commits: CommitSelected[]) {