From 765639f98c550c2afb8f47d8f92dc140d213f38c Mon Sep 17 00:00:00 2001 From: Peiman Jafari Date: Tue, 15 Mar 2022 08:50:53 -0700 Subject: [PATCH] rebase Signed-off-by: Peiman Jafari --- .changeset/fuzzy-swans-remain.md | 6 + plugins/scaffolder-backend/api-report.md | 1 + .../actions/builtin/createBuiltinActions.ts | 5 + .../builtin/github/githubIssuesLabel.test.ts | 89 ++++++++++++++ .../builtin/github/githubIssuesLabel.ts | 110 ++++++++++++++++++ .../actions/builtin/github/index.ts | 1 + .../builtin/publish/githubPullRequest.test.ts | 13 ++- .../builtin/publish/githubPullRequest.ts | 11 +- 8 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 .changeset/fuzzy-swans-remain.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts diff --git a/.changeset/fuzzy-swans-remain.md b/.changeset/fuzzy-swans-remain.md new file mode 100644 index 0000000000..32fe21ffe7 --- /dev/null +++ b/.changeset/fuzzy-swans-remain.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +GitHub issues label action:'github:issues:label' for Backstage plugin Scaffolder has been added. +'pullRequestNumber' output has been added to 'publish:github:pull-request' action. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index c0d3595503..cbc3cd1885 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -368,6 +368,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..2148298b49 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubIssuesLabel.ts @@ -0,0 +1,110 @@ +/* + * 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; + defaultWebhookSecret?: string; + 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 new 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: 'integer', + }, + 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); }