diff --git a/.changeset/fuzzy-swans-remain.md b/.changeset/fuzzy-swans-remain.md new file mode 100644 index 0000000000..0446470307 --- /dev/null +++ b/.changeset/fuzzy-swans-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Added new `github:issues:label` action to apply labels to issues, and also output `pullRequestNumber` from `publish:github:pull-request`. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index c0d3595503..d454fe2e7b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -137,6 +137,17 @@ export function createGithubActionsDispatchAction(options: { token?: string | undefined; }>; +// @public +export function createGithubIssuesLabelAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction<{ + repoUrl: string; + number: number; + labels: string[]; + token?: string | undefined; +}>; + // @public export interface CreateGithubPullRequestActionOptions { clientFactory?: ( @@ -368,6 +379,7 @@ export interface OctokitWithPullRequestPluginClient { createPullRequest(options: createPullRequest.Options): Promise<{ data: { html_url: string; + number: number; }; } | null>; } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 250a2b4b9e..93c5991ca9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -45,6 +45,7 @@ import { import { createGithubActionsDispatchAction, createGithubWebhookAction, + createGithubIssuesLabelAction, } from './github'; import { TemplateFilter } from '../../../lib'; import { TemplateAction } from '../types'; @@ -145,6 +146,10 @@ export const createBuiltinActions = ( integrations, githubCredentialsProvider, }), + createGithubIssuesLabelAction({ + integrations, + githubCredentialsProvider, + }), ]; return actions as TemplateAction[]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts new file mode 100644 index 0000000000..5518662e45 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { createGithubIssuesLabelAction } from './githubIssuesLabel'; +import { + ScmIntegrations, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, +} from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { TemplateAction } from '../..'; + +const mockOctokit = { + rest: { + issues: { + addLabels: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:issues:label', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + number: '1', + labels: ['label1', 'label2'], + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubIssuesLabelAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should call the githubApi for adding labels', async () => { + await action.handler(mockContext); + expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + issue_number: '1', + labels: ['label1', 'label2'], + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts new file mode 100644 index 0000000000..9c849edb6f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { createTemplateAction } from '../../createTemplateAction'; +import { assertError, InputError } from '@backstage/errors'; +import { Octokit } from 'octokit'; +import { getOctokitOptions } from './helpers'; +import { parseRepoUrl } from '../publish/util'; + +/** + * Adds labels to a pull request or issue on GitHub + * @public + */ +export function createGithubIssuesLabelAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + number: number; + labels: string[]; + token?: string; + }>({ + id: 'github:issues:label', + description: 'Adds labels to a pull request or issue on GitHub.', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'number', 'labels'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the repository name and 'owner' is an organization or username`, + type: 'string', + }, + number: { + title: 'Pull Request or issue number', + description: 'The pull request or issue number to add labels to', + type: 'number', + }, + labels: { + title: 'Labels', + description: 'The labels to add to the pull request or issue', + type: 'array', + items: { + type: 'string', + }, + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The GITHUB_TOKEN to use for authorization to GitHub', + }, + }, + }, + }, + async handler(ctx) { + const { repoUrl, number, labels, token: providedToken } = ctx.input; + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const client = new Octokit( + await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + repoUrl: repoUrl, + token: providedToken, + }), + ); + + try { + await client.rest.issues.addLabels({ + owner, + repo, + issue_number: number, + labels, + }); + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: adding labels to issue: '${number}' on repo: '${repo}', ${e.message}`, + ); + } + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index 07614e8a03..c5788afc3a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -16,3 +16,4 @@ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; export { createGithubWebhookAction } from './githubWebhook'; +export { createGithubIssuesLabelAction } from './githubIssuesLabel'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index 9956f16003..464498c0c4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -58,6 +58,7 @@ describe('createPublishGithubPullRequestAction', () => { status: 201, data: { html_url: 'https://github.com/myorg/myrepo/pull/123', + number: 123, }, }; }), @@ -123,13 +124,14 @@ describe('createPublishGithubPullRequestAction', () => { }); }); - it('creates outputs for the url', async () => { + it('creates outputs for the pull request url and number', async () => { await instance.handler(ctx); expect(ctx.output).toHaveBeenCalledWith( 'remoteUrl', 'https://github.com/myorg/myrepo/pull/123', ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); afterEach(() => { mockFs.restore(); @@ -254,13 +256,14 @@ describe('createPublishGithubPullRequestAction', () => { }); }); - it('creates outputs for the url', async () => { + it('creates outputs for the pull request url and number', async () => { await instance.handler(ctx); expect(ctx.output).toHaveBeenCalledWith( 'remoteUrl', 'https://github.com/myorg/myrepo/pull/123', ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); afterEach(() => { mockFs.restore(); @@ -322,13 +325,14 @@ describe('createPublishGithubPullRequestAction', () => { }); }); - it('creates outputs for the url', async () => { + it('creates outputs for the pull request url and number', async () => { await instance.handler(ctx); expect(ctx.output).toHaveBeenCalledWith( 'remoteUrl', 'https://github.com/myorg/myrepo/pull/123', ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); afterEach(() => { mockFs.restore(); @@ -390,13 +394,14 @@ describe('createPublishGithubPullRequestAction', () => { }); }); - it('creates outputs for the url', async () => { + it('creates outputs for the pull request url and number', async () => { await instance.handler(ctx); expect(ctx.output).toHaveBeenCalledWith( 'remoteUrl', 'https://github.com/myorg/myrepo/pull/123', ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); afterEach(() => { mockFs.restore(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index d46457ee1b..30293f2654 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -37,7 +37,10 @@ class GithubResponseError extends CustomErrorBase {} /** @public */ export interface OctokitWithPullRequestPluginClient { createPullRequest(options: createPullRequest.Options): Promise<{ - data: { html_url: string }; + data: { + html_url: string; + number: number; + }; } | null>; } @@ -169,6 +172,11 @@ export const createPublishGithubPullRequestAction = ({ title: 'Pull Request URL', description: 'Link to the pull request in Github', }, + pullRequestNumber: { + type: 'number', + title: 'Pull Request Number', + description: 'The pull request number', + }, }, }, }, @@ -263,6 +271,7 @@ export const createPublishGithubPullRequestAction = ({ } ctx.output('remoteUrl', response.data.html_url); + ctx.output('pullRequestNumber', response.data.number); } catch (e) { throw new GithubResponseError('Pull request creation failed', e); }