From 24e5c11a755625534c8beec71ebea7495d2a840d Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 6 Dec 2023 11:20:46 +0000 Subject: [PATCH 01/14] feat(bitbucketCloudPipelinesRun): adds new TemplateAction for running a bitbucket pipeline Signed-off-by: Matthew Prinold --- ...itbucketCloudPipelinesRun.examples.test.ts | 130 ++++++++++++++++++ .../bitbucketCloudPipelinesRun.examples.ts | 37 +++++ .../bitbucketCloudPipelinesRun.test.ts | 128 +++++++++++++++++ .../bitbucketCloudPipelinesRun.ts | 102 ++++++++++++++ 4 files changed, 397 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts new file mode 100644 index 0000000000..6205f04400 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { Writable } from 'stream'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; +import fetch from 'node-fetch'; +import yaml from 'yaml'; +import { examples } from './bitbucketCloudPipelinesRun.examples'; +import { json } from 'stream/consumers'; +import { object } from 'zod'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +const createdResponse = { + type: '', + uuid: '', + build_number: 33, + creator: { + type: '', + }, + repository: { + type: '', + }, + target: { + type: '', + }, + trigger: { + type: '', + }, + state: { + type: '', + }, + created_on: '', + completed_on: '', + build_seconds_used: 50, +}; + +describe('bitbucket:pipelines:run', () => { + const logStream = { + write: jest.fn(), + } as jest.Mocked> as jest.Mocked; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + let action: TemplateAction; + + const mockContext = { + input: {}, + baseUrl: 'somebase', + workspacePath, + logger: getVoidLogger(), + logStream, + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + const body = { + type: '', + uuid: '', + build_number: 33, + creator: { + type: '', + }, + repository: { + type: '', + }, + target: { + type: '', + }, + trigger: { + type: '', + }, + state: { + type: '', + }, + created_on: '', + completed_on: '', + build_seconds_used: 50, + }; + + beforeEach(() => { + jest.resetAllMocks(); + action = createBitbucketPipelinesRunAction(); + }); + + it('should call the bitbucket api for running a pipeline', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[0].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify(body), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts new file mode 100644 index 0000000000..81b8603c35 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts @@ -0,0 +1,37 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: '', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts new file mode 100644 index 0000000000..741ae02e92 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { Writable } from 'stream'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; +import fetch from 'node-fetch'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +const createdResponse = { + type: '', + uuid: '', + build_number: 33, + creator: { + type: '', + }, + repository: { + type: '', + }, + target: { + type: '', + }, + trigger: { + type: '', + }, + state: { + type: '', + }, + created_on: '', + completed_on: '', + build_seconds_used: 50, +}; + +describe('bitbucket:pipelines:run', () => { + const logStream = { + write: jest.fn(), + } as jest.Mocked> as jest.Mocked; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + let action: TemplateAction; + + const mockContext = { + input: {}, + baseUrl: 'somebase', + workspacePath, + logger: getVoidLogger(), + logStream, + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + const body = { + type: '', + uuid: '', + build_number: 33, + creator: { + type: '', + }, + repository: { + type: '', + }, + target: { + type: '', + }, + trigger: { + type: '', + }, + state: { + type: '', + }, + created_on: '', + completed_on: '', + build_seconds_used: 50, + }; + + beforeEach(() => { + jest.resetAllMocks(); + action = createBitbucketPipelinesRunAction(); + }); + + it('should call the bitbucket api for running a pipeline', async () => { + const workspace = 'test-workspace'; + const repo_slug = 'test-repo-slug'; + const ctx = Object.assign({}, mockContext, { + input: { workspace, repo_slug }, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify(body), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts new file mode 100644 index 0000000000..b732ce2689 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts @@ -0,0 +1,102 @@ +/* + * 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 { examples } from './bitbucketCloudPipelinesRun.examples'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import fetch from 'node-fetch'; + +const id = 'bitbucket:pipelines:run'; +/** + * Creates a new action that triggers a run of a bitbucket pipeline + * + * @public + */ +export const createBitbucketPipelinesRunAction = () => { + return createTemplateAction<{ + workspace: string; + repo_slug: string; + }>({ + id, + description: '', + examples, + schema: { + input: { + type: 'object', + required: ['workspace', 'repo_url'], + properties: { + workspace: { + title: 'Workspace', + description: `This can either be the workspace ID (slug) or the workspace UUID surrounded by curly-braces, for example {workspace UUID}.`, + type: 'string', + }, + repo_slug: { + title: 'The repository', + description: 'The repository name', + type: 'string', + }, + }, + }, + }, + supportsDryRun: false, + async handler(ctx) { + const { workspace, repo_slug } = ctx.input; + const bodyData = { + type: '', + uuid: '', + build_number: 33, + creator: { + type: '', + }, + repository: { + type: '', + }, + target: { + type: '', + }, + trigger: { + type: '', + }, + state: { + type: '', + }, + created_on: '', + completed_on: '', + build_seconds_used: 50, + }; + try { + const response = await fetch( + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, + { + method: 'POST', + headers: { + Authorization: 'Bearer testToken', + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(bodyData), + }, + ); + ctx.logStream.write( + `Response: ${response.status} ${response.statusText}`, + ); + const data = await response.json(); + ctx.logStream.write(data); + } catch (err) { + ctx.logStream.write(err); + } + }, + }); +}; From e2c90457f3aa90e37e1ea3fa7eb7091e6c5d9341 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Thu, 7 Dec 2023 13:21:52 +0000 Subject: [PATCH 02/14] feat(bitbucketCloudPipelinesRun): defines input schema for the request body Signed-off-by: Matthew Prinold --- ...itbucketCloudPipelinesRun.examples.test.ts | 253 ++++++++++++++++-- .../bitbucketCloudPipelinesRun.examples.ts | 167 +++++++++++- .../bitbucketCloudPipelinesRun.test.ts | 26 +- .../bitbucketCloudPipelinesRun.ts | 130 +++++++-- 4 files changed, 497 insertions(+), 79 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts index 6205f04400..e53099eb2b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts @@ -22,8 +22,6 @@ import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun' import fetch from 'node-fetch'; import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; -import { json } from 'stream/consumers'; -import { object } from 'zod'; jest.mock('node-fetch'); const { Response } = jest.requireActual('node-fetch'); @@ -70,36 +68,12 @@ describe('bitbucket:pipelines:run', () => { createTemporaryDirectory: jest.fn(), }; - const body = { - type: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - build_seconds_used: 50, - }; - beforeEach(() => { jest.resetAllMocks(); action = createBitbucketPipelinesRunAction(); }); - it('should call the bitbucket api for running a pipeline', async () => { + it('should trigger a pipeline for a branch', async () => { const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[0].example).steps[0].input, }); @@ -114,7 +88,230 @@ describe('bitbucket:pipelines:run', () => { expect(fetch).toHaveBeenCalledWith( 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', { - body: JSON.stringify(body), + body: JSON.stringify({ + target: { + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); + + it('should trigger a pipeline for a commit on a branch', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[1].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify({ + target: { + commit: { + type: 'commit', + hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956', + }, + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); + + it('should trigger a specific pipeline definition for a commit', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[2].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify({ + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_commit_target', + }, + }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); + + it('should trigger a specific pipeline definition for a commit on a branch or tag', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[3].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify({ + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + }, + }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); + + it('should trigger a custom pipeline with variables', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[4].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify({ + target: { + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + }, + variables: [ + { key: 'var1key', value: 'var1value', secured: true }, + { + key: 'var2key', + value: 'var2value', + }, + ], + }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer testToken', + 'Content-Type': 'application/json', + }, + method: 'POST', + }, + ); + expect(logStream.write).toHaveBeenCalledTimes(2); + expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); + expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + }); + + it('should trigger a pull request pipeline', async () => { + const ctx = Object.assign({}, mockContext, { + input: yaml.parse(examples[5].example).steps[0].input, + }); + (fetch as jest.MockedFunction).mockResolvedValue( + new Response(JSON.stringify(createdResponse), { + url: 'url', + status: 201, + statusText: 'Created', + }), + ); + await action.handler(ctx); + expect(fetch).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + { + body: JSON.stringify({ + target: { + type: 'pipeline_pullrequest_target', + source: 'pull-request-branch', + destination: 'master', + destination_commit: { + hash: '9f848b7', + }, + commit: { + hash: '1a372fc', + }, + pull_request: { + id: '3', + }, + selector: { + type: 'pull-requests', + pattern: '**', + }, + }, + }), headers: { Accept: 'application/json', Authorization: 'Bearer testToken', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts index 81b8603c35..d6255526af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts @@ -19,7 +19,7 @@ import yaml from 'yaml'; export const examples: TemplateExample[] = [ { - description: '', + description: 'Trigger a pipeline for a branch', example: yaml.stringify({ steps: [ { @@ -29,6 +29,171 @@ export const examples: TemplateExample[] = [ input: { workspace: 'test-workspace', repo_slug: 'test-repo-slug', + body: { + target: { + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Trigger a pipeline for a commit on a branch', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + body: { + target: { + commit: { + type: 'commit', + hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956', + }, + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Trigger a specific pipeline definition for a commit', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + body: { + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_commit_target', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Trigger a specific pipeline definition for a commit on a branch or tag', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + body: { + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Trigger a custom pipeline with variables', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + body: { + target: { + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + }, + variables: [ + { key: 'var1key', value: 'var1value', secured: true }, + { + key: 'var2key', + value: 'var2value', + }, + ], + }, + }, + }, + ], + }), + }, + { + description: 'Trigger a pull request pipeline', + example: yaml.stringify({ + steps: [ + { + action: 'bitbucket:pipelines:run', + id: 'run-bitbucket-pipeline', + name: 'Run an example bitbucket pipeline', + input: { + workspace: 'test-workspace', + repo_slug: 'test-repo-slug', + body: { + target: { + type: 'pipeline_pullrequest_target', + source: 'pull-request-branch', + destination: 'master', + destination_commit: { + hash: '9f848b7', + }, + commit: { + hash: '1a372fc', + }, + pull_request: { + id: '3', + }, + selector: { + type: 'pull-requests', + pattern: '**', + }, + }, + }, }, }, ], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts index 741ae02e92..d5f9da2f5d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts @@ -66,30 +66,6 @@ describe('bitbucket:pipelines:run', () => { createTemporaryDirectory: jest.fn(), }; - const body = { - type: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - build_seconds_used: 50, - }; - beforeEach(() => { jest.resetAllMocks(); action = createBitbucketPipelinesRunAction(); @@ -112,7 +88,7 @@ describe('bitbucket:pipelines:run', () => { expect(fetch).toHaveBeenCalledWith( 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', { - body: JSON.stringify(body), + body: {}, headers: { Accept: 'application/json', Authorization: 'Bearer testToken', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts index b732ce2689..fa6043621d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts @@ -28,6 +28,7 @@ export const createBitbucketPipelinesRunAction = () => { return createTemplateAction<{ workspace: string; repo_slug: string; + body?: object; }>({ id, description: '', @@ -47,35 +48,114 @@ export const createBitbucketPipelinesRunAction = () => { description: 'The repository name', type: 'string', }, + body: { + title: '', + description: '', + type: 'object', + properties: { + target: { + title: 'target', + type: 'object', + properties: { + ref_type: { + title: 'ref_type', + type: 'string', + }, + type: { + title: 'type', + type: 'string', + }, + ref_name: { + title: 'ref_name', + type: 'string', + }, + source: { + title: 'source', + type: 'string', + }, + destination: { + title: 'destination', + type: 'string', + }, + destination_commit: { + title: 'destination_commit', + type: 'object', + properties: { + hash: { + title: 'hash', + type: 'string', + }, + }, + }, + commit: { + title: 'commit', + type: 'object', + properties: { + type: { + title: 'type', + type: 'string', + }, + hash: { + title: 'hash', + type: 'string', + }, + }, + }, + selector: { + title: 'selector', + type: 'object', + properties: { + type: { + title: 'type', + type: 'string', + }, + pattern: { + title: 'pattern', + type: 'string', + }, + }, + }, + pull_request: { + title: 'pull_request', + type: 'object', + properties: { + id: { + title: 'id', + type: 'string', + }, + }, + }, + }, + }, + variables: { + title: 'variables', + type: 'array', + items: { + type: 'object', + properties: { + key: { + title: 'key', + type: 'string', + }, + value: { + title: 'value', + type: 'string', + }, + secured: { + title: 'secured', + type: 'boolean', + }, + }, + }, + }, + }, + }, }, }, }, supportsDryRun: false, async handler(ctx) { - const { workspace, repo_slug } = ctx.input; - const bodyData = { - type: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - build_seconds_used: 50, - }; + const { workspace, repo_slug, body } = ctx.input; try { const response = await fetch( `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, @@ -86,7 +166,7 @@ export const createBitbucketPipelinesRunAction = () => { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify(bodyData), + body: JSON.stringify(body) ?? {}, }, ); ctx.logStream.write( From 76e7e5b26546520ecd9bef182b2041c502242322 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Tue, 19 Dec 2023 09:10:12 +0000 Subject: [PATCH 03/14] refactor(BitbucketCloudPipelinesRun): moves action files to new module Signed-off-by: Matthew Prinold --- .../src/actions}/bitbucketCloudPipelinesRun.examples.test.ts | 0 .../src/actions}/bitbucketCloudPipelinesRun.examples.ts | 0 .../src/actions}/bitbucketCloudPipelinesRun.test.ts | 0 .../src/actions}/bitbucketCloudPipelinesRun.ts | 0 plugins/scaffolder-backend-module-bitbucket/src/actions/index.ts | 1 + 5 files changed, 1 insertion(+) rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud => scaffolder-backend-module-bitbucket/src/actions}/bitbucketCloudPipelinesRun.examples.test.ts (100%) rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud => scaffolder-backend-module-bitbucket/src/actions}/bitbucketCloudPipelinesRun.examples.ts (100%) rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud => scaffolder-backend-module-bitbucket/src/actions}/bitbucketCloudPipelinesRun.test.ts (100%) rename plugins/{scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud => scaffolder-backend-module-bitbucket/src/actions}/bitbucketCloudPipelinesRun.ts (100%) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.test.ts rename to plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.examples.ts rename to plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.test.ts rename to plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/bitbucketCloud/bitbucketCloudPipelinesRun.ts rename to plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/index.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/index.ts index 1093b07785..9b009255fd 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/index.ts @@ -17,3 +17,4 @@ export * from './bitbucket'; export * from './bitbucketCloud'; export * from './bitbucketServer'; export * from './bitbucketServerPullRequest'; +export { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; From 8de491f77a56b7d747a5e2d83e5f7fc357406077 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Tue, 19 Dec 2023 09:46:18 +0000 Subject: [PATCH 04/14] refactor(bitbucketCloudPipelinesRun): extracts out schema input properties Signed-off-by: Matthew Prinold --- .../src/actions/bitbucketCloudPipelinesRun.ts | 116 +------------- .../src/actions/inputProperties.ts | 148 ++++++++++++++++++ 2 files changed, 152 insertions(+), 112 deletions(-) create mode 100644 plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts index fa6043621d..d523cb6c6e 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts @@ -17,6 +17,7 @@ import { examples } from './bitbucketCloudPipelinesRun.examples'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch from 'node-fetch'; +import * as inputProps from './inputProperties'; const id = 'bitbucket:pipelines:run'; /** @@ -38,118 +39,9 @@ export const createBitbucketPipelinesRunAction = () => { type: 'object', required: ['workspace', 'repo_url'], properties: { - workspace: { - title: 'Workspace', - description: `This can either be the workspace ID (slug) or the workspace UUID surrounded by curly-braces, for example {workspace UUID}.`, - type: 'string', - }, - repo_slug: { - title: 'The repository', - description: 'The repository name', - type: 'string', - }, - body: { - title: '', - description: '', - type: 'object', - properties: { - target: { - title: 'target', - type: 'object', - properties: { - ref_type: { - title: 'ref_type', - type: 'string', - }, - type: { - title: 'type', - type: 'string', - }, - ref_name: { - title: 'ref_name', - type: 'string', - }, - source: { - title: 'source', - type: 'string', - }, - destination: { - title: 'destination', - type: 'string', - }, - destination_commit: { - title: 'destination_commit', - type: 'object', - properties: { - hash: { - title: 'hash', - type: 'string', - }, - }, - }, - commit: { - title: 'commit', - type: 'object', - properties: { - type: { - title: 'type', - type: 'string', - }, - hash: { - title: 'hash', - type: 'string', - }, - }, - }, - selector: { - title: 'selector', - type: 'object', - properties: { - type: { - title: 'type', - type: 'string', - }, - pattern: { - title: 'pattern', - type: 'string', - }, - }, - }, - pull_request: { - title: 'pull_request', - type: 'object', - properties: { - id: { - title: 'id', - type: 'string', - }, - }, - }, - }, - }, - variables: { - title: 'variables', - type: 'array', - items: { - type: 'object', - properties: { - key: { - title: 'key', - type: 'string', - }, - value: { - title: 'value', - type: 'string', - }, - secured: { - title: 'secured', - type: 'boolean', - }, - }, - }, - }, - }, - }, + workspace: inputProps.workspace, + repo_slug: inputProps.repo_slug, + body: inputProps.pipelinesRunBody, }, }, }, diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts new file mode 100644 index 0000000000..395c3f1c28 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts @@ -0,0 +1,148 @@ +/* + * 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. + */ + +const workspace = { + title: 'Workspace', + description: `This can either be the workspace ID (slug) or the workspace UUID surrounded by curly-braces, for example {workspace UUID}.`, + type: 'string', +}; + +const repo_slug = { + title: 'The repository', + description: 'The repository name', + type: 'string', +}; + +const ref_type = { + title: 'ref_type', + type: 'string', +}; + +const type = { + title: 'type', + type: 'string', +}; + +const ref_name = { + title: 'ref_name', + type: 'string', +}; +const source = { + title: 'source', + type: 'string', +}; +const destination = { + title: 'destination', + type: 'string', +}; +const hash = { + title: 'hash', + type: 'string', +}; + +const pattern = { + title: 'pattern', + type: 'string', +}; + +const id = { + title: 'id', + type: 'string', +}; + +const key = { + title: 'key', + type: 'string', +}; +const value = { + title: 'value', + type: 'string', +}; +const secured = { + title: 'secured', + type: 'boolean', +}; + +const destination_commit = { + title: 'destination_commit', + type: 'object', + properties: { + hash, + }, +}; + +const commit = { + title: 'commit', + type: 'object', + properties: { + type, + hash, + }, +}; + +const selector = { + title: 'selector', + type: 'object', + properties: { + type, + pattern, + }, +}; + +const pull_request = { + title: 'pull_request', + type: 'object', + properties: { + id, + }, +}; + +const pipelinesRunBody = { + title: '', + description: '', + type: 'object', + properties: { + target: { + title: 'target', + type: 'object', + properties: { + ref_type, + type, + ref_name, + source, + destination, + destination_commit, + commit, + selector, + pull_request, + }, + }, + variables: { + title: 'variables', + type: 'array', + items: { + type: 'object', + properties: { + key, + value, + secured, + }, + }, + }, + }, +}; + +export { workspace, repo_slug, pipelinesRunBody }; From 315c1903c270ec6d3215897f63ce6ea42b527418 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Tue, 19 Dec 2023 12:15:56 +0000 Subject: [PATCH 05/14] refactor(bitbucketCloud): extracts function getAuthorizationHeader into helper module Signed-off-by: Matthew Prinold --- .../src/actions/bitbucketCloud.ts | 24 +----------- .../src/actions/helpers.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 plugins/scaffolder-backend-module-bitbucket/src/actions/helpers.ts diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloud.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloud.ts index c69ae0b147..e9d815f5e8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloud.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloud.ts @@ -25,6 +25,7 @@ import { import fetch, { Response, RequestInit } from 'node-fetch'; import { Config } from '@backstage/config'; +import { getAuthorizationHeader } from './helpers'; const createRepository = async (opts: { workspace: string; @@ -93,29 +94,6 @@ const createRepository = async (opts: { return { remoteUrl, repoContentsUrl }; }; -const getAuthorizationHeader = (config: { - username?: string; - appPassword?: string; - token?: string; -}) => { - if (config.username && config.appPassword) { - const buffer = Buffer.from( - `${config.username}:${config.appPassword}`, - 'utf8', - ); - - return `Basic ${buffer.toString('base64')}`; - } - - if (config.token) { - return `Bearer ${config.token}`; - } - - throw new Error( - `Authorization has not been provided for Bitbucket Cloud. Please add either username + appPassword to the Integrations config or a user login auth token`, - ); -}; - /** * Creates a new action that initializes a git repository of the content in the workspace * and publishes it to Bitbucket Cloud. diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/helpers.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/helpers.ts new file mode 100644 index 0000000000..efa6fd64f4 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/helpers.ts @@ -0,0 +1,38 @@ +/* + * 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 const getAuthorizationHeader = (config: { + username?: string; + appPassword?: string; + token?: string; +}) => { + if (config.username && config.appPassword) { + const buffer = Buffer.from( + `${config.username}:${config.appPassword}`, + 'utf8', + ); + + return `Basic ${buffer.toString('base64')}`; + } + + if (config.token) { + return `Bearer ${config.token}`; + } + + throw new Error( + `Authorization has not been provided for Bitbucket Cloud. Please add either username + appPassword to the Integrations config or a user login auth token`, + ); +}; From 2db42a19df00674c21322022b8de51b148467443 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Tue, 19 Dec 2023 12:18:11 +0000 Subject: [PATCH 06/14] feat(bitbucketCloudPipelinesRun): adds authorization headers and refactors tests to use msw Signed-off-by: Matthew Prinold --- ...itbucketCloudPipelinesRun.examples.test.ts | 441 ++++++++---------- .../bitbucketCloudPipelinesRun.test.ts | 106 ++--- .../src/actions/bitbucketCloudPipelinesRun.ts | 34 +- .../src/actions/inputProperties.ts | 8 +- 4 files changed, 262 insertions(+), 327 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index e53099eb2b..2296f5b5e8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -15,313 +15,252 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { createMockDirectory } from '@backstage/backend-test-utils'; -import { Writable } from 'stream'; -import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; -import fetch from 'node-fetch'; import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; - -jest.mock('node-fetch'); -const { Response } = jest.requireActual('node-fetch'); - -const createdResponse = { - type: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - build_seconds_used: 50, -}; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; describe('bitbucket:pipelines:run', () => { - const logStream = { - write: jest.fn(), - } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - let action: TemplateAction; + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + const action = createBitbucketPipelinesRunAction({ integrations }); const mockContext = { input: {}, - baseUrl: 'somebase', - workspacePath, + workspacePath: 'wsp', logger: getVoidLogger(), - logStream, + logStream: new PassThrough(), output: jest.fn(), createTemporaryDirectory: jest.fn(), }; + const worker = setupServer(); + setupRequestMockHandlers(worker); beforeEach(() => { jest.resetAllMocks(); - action = createBitbucketPipelinesRunAction(); }); it('should trigger a pipeline for a branch', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[0].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - ref_type: 'branch', - type: 'pipeline_ref_target', - ref_name: 'master', - }, - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); it('should trigger a pipeline for a commit on a branch', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + commit: { + type: 'commit', + hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956', + }, + ref_type: 'branch', + type: 'pipeline_ref_target', + ref_name: 'master', + }, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[1].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - commit: { - type: 'commit', - hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956', - }, - ref_type: 'branch', - type: 'pipeline_ref_target', - ref_name: 'master', - }, - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); it('should trigger a specific pipeline definition for a commit', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_commit_target', + }, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[2].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - commit: { - type: 'commit', - hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', - }, - selector: { - type: 'custom', - pattern: 'Deploy to production', - }, - type: 'pipeline_commit_target', - }, - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); it('should trigger a specific pipeline definition for a commit on a branch or tag', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + commit: { + type: 'commit', + hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', + }, + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + }, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[3].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - commit: { - type: 'commit', - hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923', - }, - selector: { - type: 'custom', - pattern: 'Deploy to production', - }, - type: 'pipeline_ref_target', - ref_name: 'master', - ref_type: 'branch', - }, - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); it('should trigger a custom pipeline with variables', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + type: 'pipeline_ref_target', + ref_name: 'master', + ref_type: 'branch', + selector: { + type: 'custom', + pattern: 'Deploy to production', + }, + }, + variables: [ + { key: 'var1key', value: 'var1value', secured: true }, + { + key: 'var2key', + value: 'var2value', + }, + ], + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[4].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - type: 'pipeline_ref_target', - ref_name: 'master', - ref_type: 'branch', - selector: { - type: 'custom', - pattern: 'Deploy to production', - }, - }, - variables: [ - { key: 'var1key', value: 'var1value', secured: true }, - { - key: 'var2key', - value: 'var2value', - }, - ], - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); it('should trigger a pull request pipeline', async () => { + expect.assertions(2); + worker.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + target: { + type: 'pipeline_pullrequest_target', + source: 'pull-request-branch', + destination: 'master', + destination_commit: { + hash: '9f848b7', + }, + commit: { + hash: '1a372fc', + }, + pull_request: { + id: '3', + }, + selector: { + type: 'pull-requests', + pattern: '**', + }, + }, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); const ctx = Object.assign({}, mockContext, { input: yaml.parse(examples[5].example).steps[0].input, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: JSON.stringify({ - target: { - type: 'pipeline_pullrequest_target', - source: 'pull-request-branch', - destination: 'master', - destination_commit: { - hash: '9f848b7', - }, - commit: { - hash: '1a372fc', - }, - pull_request: { - id: '3', - }, - selector: { - type: 'pull-requests', - pattern: '**', - }, - }, - }), - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts index d5f9da2f5d..97cdc05d2f 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -15,90 +15,64 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { createMockDirectory } from '@backstage/backend-test-utils'; -import { Writable } from 'stream'; -import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; -import fetch from 'node-fetch'; - -jest.mock('node-fetch'); -const { Response } = jest.requireActual('node-fetch'); - -const createdResponse = { - type: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - build_seconds_used: 50, -}; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; describe('bitbucket:pipelines:run', () => { - const logStream = { - write: jest.fn(), - } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - let action: TemplateAction; + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + const action = createBitbucketPipelinesRunAction({ integrations }); const mockContext = { input: {}, - baseUrl: 'somebase', - workspacePath, + workspacePath: 'wsp', logger: getVoidLogger(), - logStream, + logStream: new PassThrough(), output: jest.fn(), createTemporaryDirectory: jest.fn(), }; + const worker = setupServer(); + setupRequestMockHandlers(worker); beforeEach(() => { - jest.resetAllMocks(); - action = createBitbucketPipelinesRunAction(); + jest.clearAllMocks(); }); it('should call the bitbucket api for running a pipeline', async () => { + expect.assertions(1); const workspace = 'test-workspace'; const repo_slug = 'test-repo-slug'; - const ctx = Object.assign({}, mockContext, { + worker.use( + rest.post( + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); + + const testContext = Object.assign({}, mockContext, { input: { workspace, repo_slug }, }); - (fetch as jest.MockedFunction).mockResolvedValue( - new Response(JSON.stringify(createdResponse), { - url: 'url', - status: 201, - statusText: 'Created', - }), - ); - await action.handler(ctx); - expect(fetch).toHaveBeenCalledWith( - 'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines', - { - body: {}, - headers: { - Accept: 'application/json', - Authorization: 'Bearer testToken', - 'Content-Type': 'application/json', - }, - method: 'POST', - }, - ); - expect(logStream.write).toHaveBeenCalledTimes(2); - expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created'); - expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse); + await action.handler(testContext); }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts index d523cb6c6e..a8ad93ce9b 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts @@ -18,6 +18,9 @@ import { examples } from './bitbucketCloudPipelinesRun.examples'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fetch from 'node-fetch'; import * as inputProps from './inputProperties'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { InputError } from '@backstage/errors'; +import { getAuthorizationHeader } from './helpers'; const id = 'bitbucket:pipelines:run'; /** @@ -25,11 +28,15 @@ const id = 'bitbucket:pipelines:run'; * * @public */ -export const createBitbucketPipelinesRunAction = () => { +export const createBitbucketPipelinesRunAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; return createTemplateAction<{ workspace: string; repo_slug: string; body?: object; + token?: string; }>({ id, description: '', @@ -42,32 +49,41 @@ export const createBitbucketPipelinesRunAction = () => { workspace: inputProps.workspace, repo_slug: inputProps.repo_slug, body: inputProps.pipelinesRunBody, + token: inputProps.token, }, }, }, supportsDryRun: false, async handler(ctx) { - const { workspace, repo_slug, body } = ctx.input; + const { workspace, repo_slug, body, token } = ctx.input; + const host = 'bitbucket.org'; + const integrationConfig = integrations.bitbucketCloud.byHost(host); + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const authorization = getAuthorizationHeader( + token ? { token } : integrationConfig.config, + ); + try { const response = await fetch( `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, { method: 'POST', headers: { - Authorization: 'Bearer testToken', + Authorization: authorization, Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(body) ?? {}, }, ); - ctx.logStream.write( - `Response: ${response.status} ${response.statusText}`, - ); const data = await response.json(); - ctx.logStream.write(data); - } catch (err) { - ctx.logStream.write(err); + } catch (e) { + throw new Error(`Unable to run pipeline, ${e}`); } }, }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts index 395c3f1c28..1a4a845e63 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts @@ -76,6 +76,12 @@ const secured = { type: 'boolean', }; +const token = { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to BitBucket Cloud', +}; + const destination_commit = { title: 'destination_commit', type: 'object', @@ -145,4 +151,4 @@ const pipelinesRunBody = { }, }; -export { workspace, repo_slug, pipelinesRunBody }; +export { workspace, repo_slug, pipelinesRunBody, token }; From e7fc793e898de09879f0e11274ecc213f0c6feba Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 20 Dec 2023 11:34:01 +0000 Subject: [PATCH 07/14] feat(createBuiltinActions): adds bitbucket:pipelines:run action to built in actions Signed-off-by: Matthew Prinold --- ...itbucketCloudPipelinesRun.examples.test.ts | 20 +++-- .../bitbucketCloudPipelinesRun.test.ts | 86 ++++++++++++++++++- .../src/actions/bitbucketCloudPipelinesRun.ts | 51 ++++++++--- .../actions/builtin/createBuiltinActions.ts | 4 + 4 files changed, 139 insertions(+), 22 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 2296f5b5e8..90ae2c08b9 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -47,6 +47,14 @@ describe('bitbucket:pipelines:run', () => { output: jest.fn(), createTemporaryDirectory: jest.fn(), }; + const responseJson = { + repository: { + links: { + html: { href: 'https://bitbucket.org/test-workspace/test-repo-slug' }, + }, + }, + build_number: 1, + }; const worker = setupServer(); setupRequestMockHandlers(worker); @@ -71,7 +79,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -103,7 +111,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -137,7 +145,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -173,7 +181,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -212,7 +220,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -253,7 +261,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts index 97cdc05d2f..dc76a79898 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -45,6 +45,16 @@ describe('bitbucket:pipelines:run', () => { output: jest.fn(), createTemporaryDirectory: jest.fn(), }; + const workspace = 'test-workspace'; + const repo_slug = 'test-repo-slug'; + const responseJson = { + repository: { + links: { + html: { href: 'https://bitbucket.org/test-workspace/test-repo-slug' }, + }, + }, + build_number: 1, + }; const worker = setupServer(); setupRequestMockHandlers(worker); @@ -52,10 +62,27 @@ describe('bitbucket:pipelines:run', () => { jest.clearAllMocks(); }); - it('should call the bitbucket api for running a pipeline', async () => { + it('should throw if there is no integration credentials or token provided', async () => { + const configNoCreds = new ConfigReader({ + integrations: { + bitbucketCloud: [], + }, + }); + + const integrationsNoCreds = ScmIntegrations.fromConfig(configNoCreds); + const actionNoCreds = createBitbucketPipelinesRunAction({ + integrations: integrationsNoCreds, + }); + const testContext = Object.assign({}, mockContext, { + input: { workspace, repo_slug }, + }); + await expect(actionNoCreds.handler(testContext)).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + + it('should call the bitbucket api for running a pipeline with integration credentials', async () => { expect.assertions(1); - const workspace = 'test-workspace'; - const repo_slug = 'test-repo-slug'; worker.use( rest.post( `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, @@ -64,7 +91,7 @@ describe('bitbucket:pipelines:run', () => { return res( ctx.status(201), ctx.set('Content-Type', 'application/json'), - ctx.json({}), + ctx.json(responseJson), ); }, ), @@ -75,4 +102,55 @@ describe('bitbucket:pipelines:run', () => { }); await action.handler(testContext); }); + + it('should call the bitbucket api for running a pipeline with input token', async () => { + expect.assertions(1); + worker.use( + rest.post( + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer abc'); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseJson), + ); + }, + ), + ); + + const testContext = Object.assign({}, mockContext, { + input: { workspace, repo_slug, token: 'abc' }, + }); + await action.handler(testContext); + }); + + it('should call outputs with the correct data', async () => { + worker.use( + rest.post( + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseJson), + ); + }, + ), + ); + + const testContext = Object.assign({}, mockContext, { + input: { workspace, repo_slug, token: 'abc' }, + }); + await action.handler(testContext); + expect(testContext.output).toHaveBeenCalledWith('buildNumber', 1); + expect(testContext.output).toHaveBeenCalledWith( + 'repoUrl', + 'https://bitbucket.org/test-workspace/test-repo-slug', + ); + expect(testContext.output).toHaveBeenCalledWith( + 'pipelinesUrl', + 'https://bitbucket.org/test-workspace/test-repo-slug/pipelines', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts index a8ad93ce9b..594da76971 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts @@ -16,10 +16,9 @@ import { examples } from './bitbucketCloudPipelinesRun.examples'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import fetch from 'node-fetch'; +import fetch, { Response } from 'node-fetch'; import * as inputProps from './inputProperties'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { InputError } from '@backstage/errors'; import { getAuthorizationHeader } from './helpers'; const id = 'bitbucket:pipelines:run'; @@ -44,7 +43,7 @@ export const createBitbucketPipelinesRunAction = (options: { schema: { input: { type: 'object', - required: ['workspace', 'repo_url'], + required: ['workspace', 'repo_slug'], properties: { workspace: inputProps.workspace, repo_slug: inputProps.repo_slug, @@ -52,24 +51,36 @@ export const createBitbucketPipelinesRunAction = (options: { token: inputProps.token, }, }, + output: { + type: 'object', + properties: { + buildNumber: { + title: 'Build number', + type: 'number', + }, + repoUrl: { + title: 'A URL to the pipeline repositry', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the pipeline', + type: 'string', + }, + }, + }, }, supportsDryRun: false, async handler(ctx) { const { workspace, repo_slug, body, token } = ctx.input; const host = 'bitbucket.org'; const integrationConfig = integrations.bitbucketCloud.byHost(host); - if (!integrationConfig) { - throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, - ); - } const authorization = getAuthorizationHeader( - token ? { token } : integrationConfig.config, + token ? { token } : integrationConfig!.config, ); - + let response: Response; try { - const response = await fetch( + response = await fetch( `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`, { method: 'POST', @@ -81,10 +92,26 @@ export const createBitbucketPipelinesRunAction = (options: { body: JSON.stringify(body) ?? {}, }, ); - const data = await response.json(); } catch (e) { throw new Error(`Unable to run pipeline, ${e}`); } + + if (response.status !== 201) { + throw new Error( + `Unable to run pipeline, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const responseObject = await response.json(); + + ctx.output('buildNumber', responseObject.build_number); + ctx.output('repoUrl', responseObject.repository.links.html.href); + ctx.output( + 'pipelinesUrl', + `${responseObject.repository.links.html.href}/pipelines`, + ); }, }); }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index dfef2d3c92..a767b3cd17 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -60,6 +60,7 @@ import { createPublishBitbucketCloudAction, createPublishBitbucketServerAction, createPublishBitbucketServerPullRequestAction, + createBitbucketPipelinesRunAction, } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; import { @@ -223,6 +224,9 @@ export const createBuiltinActions = ( integrations, githubCredentialsProvider, }), + createBitbucketPipelinesRunAction({ + integrations, + }), ]; return actions as TemplateAction[]; From 54a035bde1ebfdcba7ba62846be5d6a09950996b Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 20 Dec 2023 12:18:23 +0000 Subject: [PATCH 08/14] feat(InputProperties): updates descriptions and titles Signed-off-by: Matthew Prinold --- .../src/actions/inputProperties.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts index 1a4a845e63..d37426e982 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts @@ -16,12 +16,12 @@ const workspace = { title: 'Workspace', - description: `This can either be the workspace ID (slug) or the workspace UUID surrounded by curly-braces, for example {workspace UUID}.`, + description: `The workspace name`, type: 'string', }; const repo_slug = { - title: 'The repository', + title: 'Repository name', description: 'The repository name', type: 'string', }; @@ -117,8 +117,9 @@ const pull_request = { }; const pipelinesRunBody = { - title: '', - description: '', + title: 'Request Body', + description: + 'Request body properties: see Bitbucket Cloud Rest API documentation for more details', type: 'object', properties: { target: { From 8d29882a7be8a621c178b072e501855066494630 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 20 Dec 2023 13:16:28 +0000 Subject: [PATCH 09/14] chore(api-report): rebuilds api-report Signed-off-by: Matthew Prinold --- .../api-report.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/scaffolder-backend-module-bitbucket/api-report.md b/plugins/scaffolder-backend-module-bitbucket/api-report.md index 8e58c5fe7e..0f5f09b5b8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/api-report.md +++ b/plugins/scaffolder-backend-module-bitbucket/api-report.md @@ -8,6 +8,19 @@ import { JsonObject } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +// @public +export const createBitbucketPipelinesRunAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction< + { + workspace: string; + repo_slug: string; + body?: object | undefined; + token?: string | undefined; + }, + JsonObject +>; + // @public @deprecated export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; From a694f7188d78d28a5111638e7a481ae8ccf5e644 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 20 Dec 2023 13:22:02 +0000 Subject: [PATCH 10/14] docs(Changeset): adds change set for new builtin action bitbucket:pipelines:run Signed-off-by: Matthew Prinold --- .changeset/green-dolphins-cover.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/green-dolphins-cover.md diff --git a/.changeset/green-dolphins-cover.md b/.changeset/green-dolphins-cover.md new file mode 100644 index 0000000000..cefd10e246 --- /dev/null +++ b/.changeset/green-dolphins-cover.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API From 0a36addbce6b35ea09185537c6bf5b3b96b89605 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Wed, 3 Jan 2024 09:39:12 +0000 Subject: [PATCH 11/14] refactor(bitbucketCloudPipelineRun): updates description for the action Signed-off-by: Matthew Prinold --- .../src/actions/bitbucketCloudPipelinesRun.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts index 594da76971..c44fdcc824 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts @@ -38,7 +38,7 @@ export const createBitbucketPipelinesRunAction = (options: { token?: string; }>({ id, - description: '', + description: 'Run a bitbucket cloud pipeline', examples, schema: { input: { From 32aafe024e014cb404ba2de61b1ec5908f1cf0ee Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Thu, 4 Jan 2024 10:22:53 +0000 Subject: [PATCH 12/14] docs(changeset): updates changeset from patch to minor Signed-off-by: Matthew Prinold --- .changeset/green-dolphins-cover.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/green-dolphins-cover.md b/.changeset/green-dolphins-cover.md index cefd10e246..b990a2651a 100644 --- a/.changeset/green-dolphins-cover.md +++ b/.changeset/green-dolphins-cover.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-scaffolder-backend-module-bitbucket': patch -'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': minor +'@backstage/plugin-scaffolder-backend': minor --- The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API From 4e1e124cc6b4d27b7d6a8e647dd48fbe67d4577f Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Thu, 4 Jan 2024 10:49:36 +0000 Subject: [PATCH 13/14] Revert "docs(changeset): updates changeset from patch to minor" This reverts commit 9766e3dfb65a552badf0ade43c9d1335aaa029a1. Signed-off-by: Matthew Prinold --- .changeset/green-dolphins-cover.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/green-dolphins-cover.md b/.changeset/green-dolphins-cover.md index b990a2651a..cefd10e246 100644 --- a/.changeset/green-dolphins-cover.md +++ b/.changeset/green-dolphins-cover.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-scaffolder-backend-module-bitbucket': minor -'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend': patch --- The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API From 2b1392cd3e55a8df771e5a39ae86d54b1b91e179 Mon Sep 17 00:00:00 2001 From: Matthew Prinold Date: Thu, 4 Jan 2024 10:52:11 +0000 Subject: [PATCH 14/14] docs(changeset): updates changeset from patch to minor for plugin-scaffolder-backend Signed-off-by: Matthew Prinold --- .changeset/green-dolphins-cover.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/green-dolphins-cover.md b/.changeset/green-dolphins-cover.md index cefd10e246..77c90e72f5 100644 --- a/.changeset/green-dolphins-cover.md +++ b/.changeset/green-dolphins-cover.md @@ -1,6 +1,6 @@ --- '@backstage/plugin-scaffolder-backend-module-bitbucket': patch -'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend': minor --- The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API