From c914db284f948c69d675a884b69a1c2c6663804f Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Wed, 14 Jul 2021 17:17:39 +0100 Subject: [PATCH 1/7] feat(scaffolder): add gha dispatch workflow action Signed-off-by: Andrea Falzetti --- .../builtin/ci/githubActionsDispatch.test.ts | 134 ++++++++++++++++++ .../builtin/ci/githubActionsDispatch.ts | 124 ++++++++++++++++ .../scaffolder/actions/builtin/ci/index.ts | 17 +++ .../actions/builtin/createBuiltinActions.ts | 5 + 4 files changed, 280 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts new file mode 100644 index 0000000000..606881e47a --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts @@ -0,0 +1,134 @@ +/* + * 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. + */ + +jest.mock('@octokit/rest'); +jest.mock('@backstage/integration'); + +import { createGithubActionsDispatchAction } from './githubActionsDispatch'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +// import { when } from 'jest-when'; + +describe('ci:github-actions-dispatch', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGithubActionsDispatchAction({ integrations, config }); + + const mockContext = { + input: { + owner: 'a-owner', + repoUrl: 'github.com?repo=repo&owner=owner', + repoName: 'repository-name', + workflowId: 'a-workflow-id', + branchOrTagName: 'main', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + const { mockGithubClient } = require('@octokit/rest'); + mockGithubClient.rest = { + actions: { + createWorkflowDispatch: jest.fn(), + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + // it('should throw if there is no integration config provided', async () => { + // const actionWithNoIntegrations = createGithubActionsDispatchAction({ + // integrations: { + // ...integrations, + // github: { + // list: () => [], + // byHost: jest.fn(), + // byUrl: jest.fn(), + // }, + // }, + // config, + // }); + // await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( + // /No matching integration configuration/, + // ); + // }); + + // it('should throw if there is no token in the integration config that is returned', async () => { + // const mockedProvider = jest.fn(); + // const mockedIntegrationConfig = jest.fn(); + // GithubCredentialsProvider.create.mockReturnOnce(mockedProvider); + // const mockedArray = [ + // { + // config: { + // host: 'github.com', + // }, + // }, + // ] as GitHubIntegration[]; + // const integrations1 = { + // github: { + // list: () => mockedArray, + // byHost: mockedIntegrationConfig, + // byUrl: jest.fn(), + // }, + // } as ScmIntegrationRegistry; + // const actionWithNoIntegrations = createGithubActionsDispatchAction({ + // integrations: integrations1, + // config, + // }); + // mockedProvider.mockResolvedValue(undefined); + + // await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( + // /No token available for host/, + // ); + // }); + + it('should call the githubApis for creating WorkflowDispatch', async () => { + mockGithubClient.rest.actions.createWorkflowDispatch.mockResolvedValue({ + message: 'Success', + }); + const owner = 'dx'; + const repoName = 'test1'; + const workflowId = 'dispatch_workflow'; + const branchOrTagName = 'main'; + const ctx = Object.assign({}, mockContext, { + input: { owner, repoName, workflowId, branchOrTagName }, + }); + await action.handler(ctx); + + expect( + mockGithubClient.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith({ + owner, + repo: repoName, + workflow_id: workflowId, + ref: branchOrTagName, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts new file mode 100644 index 0000000000..fbeeb4c9ac --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts @@ -0,0 +1,124 @@ +/* + * 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 { InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from '@octokit/rest'; +import { createTemplateAction } from '../../createTemplateAction'; +import { Config } from '@backstage/config'; + +const host = 'github.com'; + +export function createGithubActionsDispatchAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations } = options; + + const credentialsProviders = new Map( + integrations.github.list().map(integration => { + const provider = GithubCredentialsProvider.create(integration.config); + return [integration.config.host, provider]; + }), + ); + + return createTemplateAction<{ + owner: string; + repoName: string; + workflowId: string; + branchOrTagName: string; + }>({ + id: 'ci:github-actions-dispatch', + description: + 'Dispatches a GitHub Action workflow for a given branch or tag', + schema: { + input: { + type: 'object', + required: ['owner', 'repoName', 'workflowId', 'branchOrTagName'], + properties: { + owner: { + title: 'Repository Owner', + description: 'GitHub Org or User name owner of the repository', + type: 'string', + }, + repoName: { + title: 'Repository Name', + description: `Repo`, + type: 'string', + }, + workflowId: { + title: 'Workflow ID', + description: 'The GitHub Action Workflow filename', + type: 'string', + }, + branchOrTagName: { + title: 'Branch or Tag name', + description: + 'The git branch or tag name used to dispatch the workflow', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { owner, repoName, workflowId, branchOrTagName } = ctx.input; + + ctx.logger.info( + `Dispatching workflow ${workflowId} for repo ${owner}/${repoName} on ${branchOrTagName}`, + ); + + const credentialsProvider = credentialsProviders.get(host); + const integrationConfig = integrations.github.byHost(host); + + if (!credentialsProvider || !integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + // TODO(blam): Consider changing this API to have owner, repo interface instead of URL as the it's + // needless to create URL and then parse again the other side. + const { token } = await credentialsProvider.getCredentials({ + url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( + repoName, + )}`, + }); + + if (!token) { + throw new InputError( + `No token available for host: ${host}, with owner ${owner}, and repo ${repoName}`, + ); + } + + const client = new Octokit({ + auth: ctx.token ?? token, + baseUrl: integrationConfig.config.apiBaseUrl, + previews: ['nebula-preview'], + }); + + await client.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflowId, + ref: branchOrTagName, + }); + + ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts new file mode 100644 index 0000000000..50abd2c3dc --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { createGithubActionsDispatchAction } from './githubActionsDispatch'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index ae5a7d0697..48c13436fa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -37,6 +37,7 @@ import { createPublishGithubPullRequestAction, createPublishGitlabAction, } from './publish'; +import { createGithubActionsDispatchAction } from './ci'; export const createBuiltinActions = (options: { reader: UrlReader; @@ -91,5 +92,9 @@ export const createBuiltinActions = (options: { createCatalogWriteAction(), createFilesystemDeleteAction(), createFilesystemRenameAction(), + createGithubActionsDispatchAction({ + integrations, + config, + }), ]; }; From a370ecab04a59c4a50ba6e92b9d321532a53e240 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Thu, 15 Jul 2021 13:17:05 +0200 Subject: [PATCH 2/7] fix unit test Signed-off-by: Yogesh Lonkar Signed-off-by: Andrea Falzetti --- .../builtin/ci/githubActionsDispatch.test.ts | 157 +++++++++++------- .../builtin/ci/githubActionsDispatch.ts | 3 +- 2 files changed, 97 insertions(+), 63 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts index 606881e47a..6564671664 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts @@ -13,16 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +// @ts-nocheck -jest.mock('@octokit/rest'); jest.mock('@backstage/integration'); +jest.mock('@octokit/rest', () => ({ Octokit: jest.fn() })); import { createGithubActionsDispatchAction } from './githubActionsDispatch'; -import { ScmIntegrations } from '@backstage/integration'; +import { + ScmIntegrations, + GithubCredentialsProvider, +} from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -// import { when } from 'jest-when'; + +import { Octokit } from '@octokit/rest'; describe('ci:github-actions-dispatch', () => { const config = new ConfigReader({ @@ -34,9 +39,6 @@ describe('ci:github-actions-dispatch', () => { }, }); - const integrations = ScmIntegrations.fromConfig(config); - const action = createGithubActionsDispatchAction({ integrations, config }); - const mockContext = { input: { owner: 'a-owner', @@ -52,67 +54,101 @@ describe('ci:github-actions-dispatch', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('@octokit/rest'); - mockGithubClient.rest = { - actions: { - createWorkflowDispatch: jest.fn(), - }, - }; - beforeEach(() => { jest.resetAllMocks(); }); - // it('should throw if there is no integration config provided', async () => { - // const actionWithNoIntegrations = createGithubActionsDispatchAction({ - // integrations: { - // ...integrations, - // github: { - // list: () => [], - // byHost: jest.fn(), - // byUrl: jest.fn(), - // }, - // }, - // config, - // }); - // await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( - // /No matching integration configuration/, - // ); - // }); + it('should throw if there is no integration config provided', async () => { + const integrations = { + github: { + list: () => [], + byHost: jest.fn(), + byUrl: jest.fn(), + }, + } as ScmIntegrations; + const actionWithNoIntegrations = createGithubActionsDispatchAction({ + integrations, + config, + }); + await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( + /No matching integration configuration/, + ); + }); - // it('should throw if there is no token in the integration config that is returned', async () => { - // const mockedProvider = jest.fn(); - // const mockedIntegrationConfig = jest.fn(); - // GithubCredentialsProvider.create.mockReturnOnce(mockedProvider); - // const mockedArray = [ - // { - // config: { - // host: 'github.com', - // }, - // }, - // ] as GitHubIntegration[]; - // const integrations1 = { - // github: { - // list: () => mockedArray, - // byHost: mockedIntegrationConfig, - // byUrl: jest.fn(), - // }, - // } as ScmIntegrationRegistry; - // const actionWithNoIntegrations = createGithubActionsDispatchAction({ - // integrations: integrations1, - // config, - // }); - // mockedProvider.mockResolvedValue(undefined); - - // await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( - // /No token available for host/, - // ); - // }); + it('should throw if no token is provided', async () => { + const integrations = { + github: { + list: () => [ + { + config: { + host: 'github.com', + }, + }, + ], + byHost: () => ({ + config: { + apiBaseUrl: 'https://api.github.com', + }, + }), + byUrl: jest.fn(), + }, + } as ScmIntegrations; + const mockedCredentialsProvider = { + getCredentials: jest.fn(), + }; + mockedCredentialsProvider.getCredentials.mockResolvedValue({}); + GithubCredentialsProvider.create.mockReturnValueOnce( + mockedCredentialsProvider, + ); + const actionWithNoIntegrations = createGithubActionsDispatchAction({ + integrations, + config, + }); + await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( + /No token available for host/, + ); + }); it('should call the githubApis for creating WorkflowDispatch', async () => { - mockGithubClient.rest.actions.createWorkflowDispatch.mockResolvedValue({ + const integrations = { + github: { + list: () => [ + { + config: { + host: 'github.com', + }, + }, + ], + byHost: () => ({ + config: { + apiBaseUrl: 'https://api.github.com', + }, + }), + byUrl: jest.fn(), + }, + } as ScmIntegrations; + const mockedCredentialsProvider = { + getCredentials: jest.fn(), + }; + mockedCredentialsProvider.getCredentials.mockResolvedValue({ + token: 'test-token', + }); + GithubCredentialsProvider.create.mockReturnValueOnce( + mockedCredentialsProvider, + ); + const mockedCreateWorkflowDispatch = jest.fn(); + mockedCreateWorkflowDispatch.mockResolvedValue({ message: 'Success', }); + Octokit.mockImplementation(() => { + return { + rest: { + actions: { + createWorkflowDispatch: mockedCreateWorkflowDispatch, + }, + }, + }; + }); const owner = 'dx'; const repoName = 'test1'; const workflowId = 'dispatch_workflow'; @@ -120,11 +156,10 @@ describe('ci:github-actions-dispatch', () => { const ctx = Object.assign({}, mockContext, { input: { owner, repoName, workflowId, branchOrTagName }, }); + const action = createGithubActionsDispatchAction({ integrations, config }); await action.handler(ctx); - expect( - mockGithubClient.rest.actions.createWorkflowDispatch, - ).toHaveBeenCalledWith({ + expect(mockedCreateWorkflowDispatch).toHaveBeenCalledWith({ owner, repo: repoName, workflow_id: workflowId, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts index fbeeb4c9ac..e720695b13 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts @@ -104,7 +104,6 @@ export function createGithubActionsDispatchAction(options: { `No token available for host: ${host}, with owner ${owner}, and repo ${repoName}`, ); } - const client = new Octokit({ auth: ctx.token ?? token, baseUrl: integrationConfig.config.apiBaseUrl, @@ -113,7 +112,7 @@ export function createGithubActionsDispatchAction(options: { await client.rest.actions.createWorkflowDispatch({ owner, - repo, + repo: repoName, workflow_id: workflowId, ref: branchOrTagName, }); From c73f53bc2f0eeec75e28c7633ce1b38421e3242f Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Thu, 15 Jul 2021 15:29:30 +0100 Subject: [PATCH 3/7] docs: add changeset Signed-off-by: Andrea Falzetti --- .changeset/spotty-actors-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-actors-invite.md diff --git a/.changeset/spotty-actors-invite.md b/.changeset/spotty-actors-invite.md new file mode 100644 index 0000000000..96f179c475 --- /dev/null +++ b/.changeset/spotty-actors-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add new built-in action ci:github-actions-dispatch From 2ca8855b0d5257f903e63ed527f889f50cb69633 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Fri, 16 Jul 2021 09:31:36 +0100 Subject: [PATCH 4/7] fix: remove ctx.token Signed-off-by: Andrea Falzetti --- .../src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts index e720695b13..99e745f852 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts @@ -105,7 +105,7 @@ export function createGithubActionsDispatchAction(options: { ); } const client = new Octokit({ - auth: ctx.token ?? token, + auth: token, baseUrl: integrationConfig.config.apiBaseUrl, previews: ['nebula-preview'], }); From f928a8f9bfcf1ecd84404ca2c0d62ab7b5be8fc4 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Fri, 16 Jul 2021 09:44:53 +0100 Subject: [PATCH 5/7] chore: remove comment Signed-off-by: Andrea Falzetti --- .../src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts index 99e745f852..d7a5a26ccb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts @@ -91,8 +91,6 @@ export function createGithubActionsDispatchAction(options: { ); } - // TODO(blam): Consider changing this API to have owner, repo interface instead of URL as the it's - // needless to create URL and then parse again the other side. const { token } = await credentialsProvider.getCredentials({ url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( repoName, @@ -104,6 +102,7 @@ export function createGithubActionsDispatchAction(options: { `No token available for host: ${host}, with owner ${owner}, and repo ${repoName}`, ); } + const client = new Octokit({ auth: token, baseUrl: integrationConfig.config.apiBaseUrl, From 8fb0874dd44c1098d0120fb9e93bb26e9995f250 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Wed, 21 Jul 2021 16:50:01 +0100 Subject: [PATCH 6/7] refactor: use repoUrl Signed-off-by: Andrea Falzetti --- .../__mocks__/@octokit/rest/index.ts | 5 + .../builtin/ci/githubActionsDispatch.test.ts | 169 ------------------ .../actions/builtin/createBuiltinActions.ts | 3 +- .../github/githubActionsDispatch.test.ts | 117 ++++++++++++ .../{ci => github}/githubActionsDispatch.ts | 35 ++-- .../actions/builtin/{ci => github}/index.ts | 0 .../src/scaffolder/actions/builtin/index.ts | 2 +- 7 files changed, 138 insertions(+), 193 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts rename plugins/scaffolder-backend/src/scaffolder/actions/builtin/{ci => github}/githubActionsDispatch.ts (80%) rename plugins/scaffolder-backend/src/scaffolder/actions/builtin/{ci => github}/index.ts (100%) diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts index 75cdfc208f..7505c3a30e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts @@ -15,6 +15,11 @@ */ export const mockGithubClient = { + rest: { + actions: { + createWorkflowDispatch: jest.fn(), + }, + }, repos: { createInOrg: jest.fn(), createForAuthenticatedUser: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts deleted file mode 100644 index 6564671664..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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. - */ -// @ts-nocheck - -jest.mock('@backstage/integration'); -jest.mock('@octokit/rest', () => ({ Octokit: jest.fn() })); - -import { createGithubActionsDispatchAction } from './githubActionsDispatch'; -import { - ScmIntegrations, - GithubCredentialsProvider, -} from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; - -import { Octokit } from '@octokit/rest'; - -describe('ci:github-actions-dispatch', () => { - const config = new ConfigReader({ - integrations: { - github: [ - { host: 'github.com', token: 'tokenlols' }, - { host: 'ghe.github.com' }, - ], - }, - }); - - const mockContext = { - input: { - owner: 'a-owner', - repoUrl: 'github.com?repo=repo&owner=owner', - repoName: 'repository-name', - workflowId: 'a-workflow-id', - branchOrTagName: 'main', - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should throw if there is no integration config provided', async () => { - const integrations = { - github: { - list: () => [], - byHost: jest.fn(), - byUrl: jest.fn(), - }, - } as ScmIntegrations; - const actionWithNoIntegrations = createGithubActionsDispatchAction({ - integrations, - config, - }); - await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( - /No matching integration configuration/, - ); - }); - - it('should throw if no token is provided', async () => { - const integrations = { - github: { - list: () => [ - { - config: { - host: 'github.com', - }, - }, - ], - byHost: () => ({ - config: { - apiBaseUrl: 'https://api.github.com', - }, - }), - byUrl: jest.fn(), - }, - } as ScmIntegrations; - const mockedCredentialsProvider = { - getCredentials: jest.fn(), - }; - mockedCredentialsProvider.getCredentials.mockResolvedValue({}); - GithubCredentialsProvider.create.mockReturnValueOnce( - mockedCredentialsProvider, - ); - const actionWithNoIntegrations = createGithubActionsDispatchAction({ - integrations, - config, - }); - await expect(actionWithNoIntegrations.handler(mockContext)).rejects.toThrow( - /No token available for host/, - ); - }); - - it('should call the githubApis for creating WorkflowDispatch', async () => { - const integrations = { - github: { - list: () => [ - { - config: { - host: 'github.com', - }, - }, - ], - byHost: () => ({ - config: { - apiBaseUrl: 'https://api.github.com', - }, - }), - byUrl: jest.fn(), - }, - } as ScmIntegrations; - const mockedCredentialsProvider = { - getCredentials: jest.fn(), - }; - mockedCredentialsProvider.getCredentials.mockResolvedValue({ - token: 'test-token', - }); - GithubCredentialsProvider.create.mockReturnValueOnce( - mockedCredentialsProvider, - ); - const mockedCreateWorkflowDispatch = jest.fn(); - mockedCreateWorkflowDispatch.mockResolvedValue({ - message: 'Success', - }); - Octokit.mockImplementation(() => { - return { - rest: { - actions: { - createWorkflowDispatch: mockedCreateWorkflowDispatch, - }, - }, - }; - }); - const owner = 'dx'; - const repoName = 'test1'; - const workflowId = 'dispatch_workflow'; - const branchOrTagName = 'main'; - const ctx = Object.assign({}, mockContext, { - input: { owner, repoName, workflowId, branchOrTagName }, - }); - const action = createGithubActionsDispatchAction({ integrations, config }); - await action.handler(ctx); - - expect(mockedCreateWorkflowDispatch).toHaveBeenCalledWith({ - owner, - repo: repoName, - workflow_id: workflowId, - ref: branchOrTagName, - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 48c13436fa..b3d1460270 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -37,7 +37,7 @@ import { createPublishGithubPullRequestAction, createPublishGitlabAction, } from './publish'; -import { createGithubActionsDispatchAction } from './ci'; +import { createGithubActionsDispatchAction } from './github'; export const createBuiltinActions = (options: { reader: UrlReader; @@ -94,7 +94,6 @@ export const createBuiltinActions = (options: { createFilesystemRenameAction(), createGithubActionsDispatchAction({ integrations, - config, }), ]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts new file mode 100644 index 0000000000..29e4338a39 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts @@ -0,0 +1,117 @@ +/* + * 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. + */ + +jest.mock('@octokit/rest'); + +import { createGithubActionsDispatchAction } from './githubActionsDispatch'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; + +describe('github:actions:dispatch', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGithubActionsDispatchAction({ integrations }); + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'a-workflow-id', + branchOrTagName: 'main', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + const { mockGithubClient } = require('@octokit/rest'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'github.com?repo=bob' }, + }), + ).rejects.toThrow(/missing owner/); + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'github.com?owner=owner' }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'missing.com?repo=bob&owner=owner' }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('should throw if there is no token in the integration config that is returned', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + repoUrl: 'ghe.github.com?repo=bob&owner=owner', + }, + }), + ).rejects.toThrow(/No token available for host/); + }); + + it('should call the githubApis for creating WorkflowDispatch', async () => { + mockGithubClient.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + foo: 'bar', + }, + }); + + const repoUrl = 'github.com?repo=repo&owner=owner'; + const workflowId = 'dispatch_workflow'; + const branchOrTagName = 'main'; + const ctx = Object.assign({}, mockContext, { + input: { repoUrl, workflowId, branchOrTagName }, + }); + await action.handler(ctx); + + expect( + mockGithubClient.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + workflow_id: workflowId, + ref: branchOrTagName, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts similarity index 80% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index d7a5a26ccb..7f48a155ed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -19,14 +19,11 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Octokit } from '@octokit/rest'; +import { parseRepoUrl } from '../publish/util'; import { createTemplateAction } from '../../createTemplateAction'; -import { Config } from '@backstage/config'; - -const host = 'github.com'; export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrationRegistry; - config: Config; }) { const { integrations } = options; @@ -38,27 +35,21 @@ export function createGithubActionsDispatchAction(options: { ); return createTemplateAction<{ - owner: string; - repoName: string; + repoUrl: string; workflowId: string; branchOrTagName: string; }>({ - id: 'ci:github-actions-dispatch', + id: 'github:actions:dispatch', description: 'Dispatches a GitHub Action workflow for a given branch or tag', schema: { input: { type: 'object', - required: ['owner', 'repoName', 'workflowId', 'branchOrTagName'], + required: ['repoUrl', 'workflowId', 'branchOrTagName'], properties: { - owner: { - title: 'Repository Owner', - description: 'GitHub Org or User name owner of the repository', - type: 'string', - }, - repoName: { - title: 'Repository Name', - description: `Repo`, + 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', }, workflowId: { @@ -76,10 +67,12 @@ export function createGithubActionsDispatchAction(options: { }, }, async handler(ctx) { - const { owner, repoName, workflowId, branchOrTagName } = ctx.input; + const { repoUrl, workflowId, branchOrTagName } = ctx.input; + + const { owner, repo, host } = parseRepoUrl(repoUrl); ctx.logger.info( - `Dispatching workflow ${workflowId} for repo ${owner}/${repoName} on ${branchOrTagName}`, + `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); const credentialsProvider = credentialsProviders.get(host); @@ -93,13 +86,13 @@ export function createGithubActionsDispatchAction(options: { const { token } = await credentialsProvider.getCredentials({ url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repoName, + repo, )}`, }); if (!token) { throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repoName}`, + `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, ); } @@ -111,7 +104,7 @@ export function createGithubActionsDispatchAction(options: { await client.rest.actions.createWorkflowDispatch({ owner, - repo: repoName, + repo, workflow_id: workflowId, ref: branchOrTagName, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/ci/index.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 2a583f369a..02c15abab1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -20,6 +20,6 @@ export * from './debug'; export * from './fetch'; export * from './filesystem'; export * from './publish'; - export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; +export * from './github'; export { runCommand } from './helpers'; From 8db7ea4c2fde31dacb31240fbae2335c43760da1 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Wed, 21 Jul 2021 17:14:09 +0100 Subject: [PATCH 7/7] docs: add api-reports Signed-off-by: Andrea Falzetti --- plugins/scaffolder-backend/api-report.md | 7 +++++++ .../src/scaffolder/actions/builtin/index.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 0e7a25534c..baf85bce63 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -107,6 +107,13 @@ export const createFilesystemDeleteAction: () => TemplateAction; // @public (undocumented) export const createFilesystemRenameAction: () => TemplateAction; +// Warning: (ae-missing-release-tag) "createGithubActionsDispatchAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createGithubActionsDispatchAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction; + // Warning: (ae-missing-release-tag) "createPublishAzureAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 02c15abab1..2265f9b4f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -20,6 +20,7 @@ export * from './debug'; export * from './fetch'; export * from './filesystem'; export * from './publish'; -export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; export * from './github'; + +export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; export { runCommand } from './helpers';