From 8fb0874dd44c1098d0120fb9e93bb26e9995f250 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Wed, 21 Jul 2021 16:50:01 +0100 Subject: [PATCH] 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';