Merge pull request #21959 from mattprinold/feat/21365-scaffolder-custom-action-bitbucket-run-pipeline

Feat/21365 scaffolder custom action bitbucket run pipeline
This commit is contained in:
Ben Lambert
2024-01-11 09:08:21 +01:00
committed by GitHub
11 changed files with 967 additions and 23 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend-module-bitbucket': patch
'@backstage/plugin-scaffolder-backend': minor
---
The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API
@@ -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;
@@ -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.
@@ -0,0 +1,274 @@
/*
* 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 { rest } from 'msw';
import { setupServer } from 'msw/node';
import { PassThrough } from 'stream';
import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
import yaml from 'yaml';
import { examples } from './bitbucketCloudPipelinesRun.examples';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
describe('bitbucket:pipelines:run', () => {
const config = new ConfigReader({
integrations: {
bitbucketCloud: [
{
username: 'u',
appPassword: 'p',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createBitbucketPipelinesRunAction({ integrations });
const mockContext = {
input: {},
workspacePath: 'wsp',
logger: getVoidLogger(),
logStream: new PassThrough(),
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);
beforeEach(() => {
jest.resetAllMocks();
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[0].example).steps[0].input,
});
await action.handler(ctx);
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[1].example).steps[0].input,
});
await action.handler(ctx);
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[2].example).steps[0].input,
});
await action.handler(ctx);
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[3].example).steps[0].input,
});
await action.handler(ctx);
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[4].example).steps[0].input,
});
await action.handler(ctx);
});
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(responseJson),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[5].example).steps[0].input,
});
await action.handler(ctx);
});
});
@@ -0,0 +1,202 @@
/*
* 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: 'Trigger a pipeline for 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: {
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: '**',
},
},
},
},
},
],
}),
},
];
@@ -0,0 +1,156 @@
/*
* 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 { rest } from 'msw';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { PassThrough } from 'stream';
import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
describe('bitbucket:pipelines:run', () => {
const config = new ConfigReader({
integrations: {
bitbucketCloud: [
{
username: 'u',
appPassword: 'p',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createBitbucketPipelinesRunAction({ integrations });
const mockContext = {
input: {},
workspacePath: 'wsp',
logger: getVoidLogger(),
logStream: new PassThrough(),
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);
beforeEach(() => {
jest.clearAllMocks();
});
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);
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(responseJson),
);
},
),
);
const testContext = Object.assign({}, mockContext, {
input: { workspace, repo_slug },
});
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',
);
});
});
@@ -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.
*/
import { examples } from './bitbucketCloudPipelinesRun.examples';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import fetch, { Response } from 'node-fetch';
import * as inputProps from './inputProperties';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { getAuthorizationHeader } from './helpers';
const id = 'bitbucket:pipelines:run';
/**
* Creates a new action that triggers a run of a bitbucket pipeline
*
* @public
*/
export const createBitbucketPipelinesRunAction = (options: {
integrations: ScmIntegrationRegistry;
}) => {
const { integrations } = options;
return createTemplateAction<{
workspace: string;
repo_slug: string;
body?: object;
token?: string;
}>({
id,
description: 'Run a bitbucket cloud pipeline',
examples,
schema: {
input: {
type: 'object',
required: ['workspace', 'repo_slug'],
properties: {
workspace: inputProps.workspace,
repo_slug: inputProps.repo_slug,
body: inputProps.pipelinesRunBody,
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);
const authorization = getAuthorizationHeader(
token ? { token } : integrationConfig!.config,
);
let response: Response;
try {
response = await fetch(
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`,
{
method: 'POST',
headers: {
Authorization: authorization,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body) ?? {},
},
);
} 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`,
);
},
});
};
@@ -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`,
);
};
@@ -17,3 +17,4 @@ export * from './bitbucket';
export * from './bitbucketCloud';
export * from './bitbucketServer';
export * from './bitbucketServerPullRequest';
export { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
@@ -0,0 +1,155 @@
/*
* 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: `The workspace name`,
type: 'string',
};
const repo_slug = {
title: 'Repository name',
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 token = {
title: 'Authentication Token',
type: 'string',
description: 'The token to use for authorization to BitBucket Cloud',
};
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: 'Request Body',
description:
'Request body properties: see Bitbucket Cloud Rest API documentation for more details',
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, token };
@@ -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[];