diff --git a/README.md b/README.md index 39cbd194..b595a7a4 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ The `createPullRequest()` method creates a GitHub Pull request with the files gi | message | `string` | The commit message for the changes. Default is `'code suggestions'`. We recommend following [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).| | force | `boolean` | Whether or not to force push the reference even if the ancestor commits differs. Default is `false`. | | fork | `boolean` | Whether or not code suggestion should be made from a fork, defaults to `true` (_Note: forking does not work when using `secrets.GITHUB_TOKEN` in an action_). | +| labels | `string[]`| The list of labels to add to the pull request. Default is none. | #### `logger` *[Logger](https://www.npmjs.com/package/@types/pino)*
@@ -231,6 +232,10 @@ Whether or not to force push a reference with different commit history before th *boolean*
Whether or not to attempt forking to a separate repository. Default value is: `true`. +#### `--labels` +*array*
+The list of labels to add to the pull request. Default is none. + ### Example ``` code-suggester pr -o foo -r bar -d 'description' -t 'title' -m 'message' --git-dir=. @@ -328,6 +333,10 @@ Whether or not maintainers can modify the pull request. Default value is: `true` *boolean*
Whether or not to attempt forking to a separate repository. Default value is: `true`. +#### `labels` +*array*
+The list of labels to add to the pull request. Default is none. + #### Example The following example is a `.github/workflows/main.yaml` file in repo `Octocat/HelloWorld`. This would add a LICENSE folder to the root `HelloWorld` repo on every pull request if it is not already there. @@ -356,6 +365,9 @@ jobs: message: 'chore(license): add license file' branch: my-branch git_dir: '.' + labels: | + bug + priority: p1 ``` ### Review a Pull Request diff --git a/src/bin/code-suggester.ts b/src/bin/code-suggester.ts index 110d1cfa..230bd915 100644 --- a/src/bin/code-suggester.ts +++ b/src/bin/code-suggester.ts @@ -93,6 +93,12 @@ yargs default: true, type: 'boolean', }, + labels: { + describe: + 'The list of labels to add to the pull request. Default is none.', + default: [], + type: 'array', + }, }) .command(REVIEW_PR_COMMAND, 'Review an open pull request', { 'upstream-repo': { diff --git a/src/bin/workflow.ts b/src/bin/workflow.ts index 561ce3e8..bc9357d0 100644 --- a/src/bin/workflow.ts +++ b/src/bin/workflow.ts @@ -40,6 +40,7 @@ export function coerceUserCreatePullRequestOptions(): CreatePullRequestUserOptio primary: yargs.argv.primary as string, maintainersCanModify: yargs.argv.maintainersCanModify as boolean, fork: yargs.argv.fork as boolean, + labels: yargs.argv.labels as string[], }; } diff --git a/src/github-handler/index.ts b/src/github-handler/index.ts index 617fd3dc..d5701367 100644 --- a/src/github-handler/index.ts +++ b/src/github-handler/index.ts @@ -17,3 +17,4 @@ export {branch} from './branch-handler'; export {commitAndPush} from './commit-and-push-handler'; export * from './pull-request-handler'; export * from './comment-handler'; +export * from './issue-handler'; diff --git a/src/github-handler/issue-handler.ts b/src/github-handler/issue-handler.ts new file mode 100644 index 00000000..391e4ca6 --- /dev/null +++ b/src/github-handler/issue-handler.ts @@ -0,0 +1,52 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {BranchDomain, RepoDomain} from '../types'; +import {Octokit} from '@octokit/rest'; +import {logger} from '../logger'; + +/** + * Create a GitHub PR on the upstream organization's repo + * Throws an error if the GitHub API fails + * @param {Octokit} octokit The authenticated octokit instance + * @param {RepoDomain} upstream The upstream repository + * @param {BranchDomain} origin The remote origin information that contains the origin branch + * @param {number} issue_number The issue number to add labels to. Can also be a PR number + * @param {string[]} labels The list of labels to apply to the issue/pull request. Default is []. the funciton will no-op. + * @returns {Promise} The list of resulting labels after the addition of the given labels + */ +async function addLabels( + octokit: Octokit, + upstream: RepoDomain, + origin: BranchDomain, + issue_number: number, + labels?: string[] +): Promise { + if (!labels || labels.length === 0) { + return []; + } + + const labelsResponseData = ( + await octokit.issues.addLabels({ + owner: upstream.owner, + repo: origin.repo, + issue_number: issue_number, + labels: labels, + }) + ).data; + logger.info(`Successfully added labels ${labels} to issue: ${issue_number}`); + return labelsResponseData.map(l => l.name); +} + +export {addLabels}; diff --git a/src/index.ts b/src/index.ts index 2fe5ac4c..fe89eb92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -176,6 +176,16 @@ async function createPullRequest( gitHubConfigs.primary ); logger.info(`Successfully opened pull request: ${prNumber}.`); + + // addLabels will no-op if options.labels is undefined or empty. + await handler.addLabels( + octokit, + upstream, + originBranch, + prNumber, + options.labels + ); + return prNumber; } diff --git a/src/types/index.ts b/src/types/index.ts index 6bc5853a..f7300dd7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -93,6 +93,8 @@ export interface CreatePullRequestUserOptions { primary?: string; // Whether or not maintainers can modify the PR. Default is true. (optional) maintainersCanModify?: boolean; + // The list of labels to apply to the newly created PR. Default is empty. (optional) + labels?: string[]; } /** diff --git a/test/cli.ts b/test/cli.ts index 4d9e7513..5453d2ca 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -103,6 +103,7 @@ describe('Mapping pr yargs to create PR options', () => { primary: 'primary', maintainersCanModify: true, fork: true, + labels: ['automerge'], }; sandbox.stub(yargs, 'argv').value({_: ['pr'], ...options}); diff --git a/test/fixtures/add-labels-response.json b/test/fixtures/add-labels-response.json new file mode 100644 index 00000000..b1664348 --- /dev/null +++ b/test/fixtures/add-labels-response.json @@ -0,0 +1,20 @@ +[ + { + "id": 208045946, + "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", + "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", + "name": "bug", + "description": "Something isn't working", + "color": "f29513", + "default": true + }, + { + "id": 208045947, + "node_id": "MDU6TGFiZWwyMDgwNDU5NDc=", + "url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement", + "name": "enhancement", + "description": "New feature or request", + "color": "a2eeef", + "default": false + } +] diff --git a/test/issues.ts b/test/issues.ts new file mode 100644 index 00000000..56524d67 --- /dev/null +++ b/test/issues.ts @@ -0,0 +1,110 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {expect} from 'chai'; +import {describe, it, before, afterEach} from 'mocha'; +import {octokit, setup} from './util'; +import * as sinon from 'sinon'; +import {addLabels} from '../src/github-handler/issue-handler'; + +before(() => { + setup(); +}); + +describe('Adding labels', async () => { + const sandbox = sinon.createSandbox(); + const upstream = {owner: 'upstream-owner', repo: 'upstream-repo'}; + const origin = { + owner: 'origin-owner', + repo: 'origin-repo', + branch: 'issues-test-branch', + }; + const issue_number = 1; + const labels = ['enhancement']; + afterEach(() => { + sandbox.restore(); + }); + + it('Invokes octokit issues add labels on an existing pull request', async () => { + // setup + const responseAddLabelsData = await import( + './fixtures/add-labels-response.json' + ); + const addLabelsResponse = { + headers: {}, + status: 200, + url: 'http://fake-url.com', + data: responseAddLabelsData, + }; + const stub = sandbox + .stub(octokit.issues, 'addLabels') + .resolves(addLabelsResponse); + // tests + const resultingLabels = await addLabels( + octokit, + upstream, + origin, + issue_number, + labels + ); + sandbox.assert.calledOnceWithExactly(stub, { + owner: upstream.owner, + repo: origin.repo, + issue_number: issue_number, + labels: labels, + }); + expect(resultingLabels).to.deep.equal(['bug', 'enhancement']); + }); + + it('No-op undefined labels', async () => { + // setup + const stub = sandbox.stub(octokit.issues, 'addLabels').resolves(); + // tests + const resultingLabels = await addLabels( + octokit, + upstream, + origin, + issue_number + ); + sandbox.assert.neverCalledWith(stub, sinon.match.any); + expect(resultingLabels).to.deep.equal([]); + }); + + it('No-op with empty labels', async () => { + // setup + const stub = sandbox.stub(octokit.issues, 'addLabels').resolves(); + // tests + const resultingLabels = await addLabels( + octokit, + upstream, + origin, + issue_number, + [] + ); + sandbox.assert.neverCalledWith(stub, sinon.match.any); + expect(resultingLabels).to.deep.equal([]); + }); + + it('Passes up the error message with a throw when octokit issues add labels fails', async () => { + // setup + const errorMsg = 'Error message'; + sandbox.stub(octokit.issues, 'addLabels').rejects(Error(errorMsg)); + try { + await addLabels(octokit, upstream, origin, issue_number, labels); + expect.fail(); + } catch (err) { + expect(err.message).to.equal(errorMsg); + } + }); +}); diff --git a/test/main-make-pr.ts b/test/main-make-pr.ts index d76ef50e..3e4055f1 100644 --- a/test/main-make-pr.ts +++ b/test/main-make-pr.ts @@ -39,6 +39,7 @@ describe('Make PR main function', () => { const primary = 'custom-primary'; const originRepo = 'Hello-World'; const originOwner = 'octocat'; + const labelsToAdd = ['automerge']; const options: CreatePullRequestUserOptions = { upstreamOwner, upstreamRepo, @@ -48,6 +49,7 @@ describe('Make PR main function', () => { force, message, primary, + labels: labelsToAdd, }; const oldHeadSha = '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; const changes: Changes = new Map(); @@ -116,6 +118,20 @@ describe('Make PR main function', () => { expect(testMaintainersCanModify).equals(maintainersCanModify); expect(testPrimary).equals(primary); }, + addLabels: ( + octokit: Octokit, + upstream: {owner: string; repo: string}, + originBranch: {owner: string; repo: string; branch: string}, + issue_number: number, + labels: string[] + ) => { + expect(originBranch.owner).equals(originOwner); + expect(originBranch.repo).equals(originRepo); + expect(originBranch.branch).equals(branch); + expect(upstream.owner).equals(upstreamOwner); + expect(upstream.repo).equals(upstreamRepo); + expect(labels).equals(labelsToAdd); + }, }; const stubMakePr = proxyquire.noCallThru()('../src/', { './github-handler': stubHelperHandlers, @@ -171,6 +187,20 @@ describe('Make PR main function', () => { expect(testMaintainersCanModify).equals(maintainersCanModify); expect(testPrimary).equals(primary); }, + addLabels: ( + octokit: Octokit, + upstream: {owner: string; repo: string}, + originBranch: {owner: string; repo: string; branch: string}, + issue_number: number, + labels: string[] + ) => { + expect(originBranch.owner).equals(upstreamOwner); + expect(originBranch.repo).equals(upstreamRepo); + expect(originBranch.branch).equals(branch); + expect(upstream.owner).equals(upstreamOwner); + expect(upstream.repo).equals(upstreamRepo); + expect(labels).equals(labelsToAdd); + }, }; const stubMakePr = proxyquire.noCallThru()('../src/', { './github-handler': stubHelperHandlers,