From bffd11fb40872dd73293f28d9562844df5af173a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 3 Sep 2024 16:28:19 +0200 Subject: [PATCH 01/14] feat: started porting the publish:bitbucketServer:pull-request action to Bitbucket Cloud Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 468 ++++++++++++++++++ .../src/actions/index.ts | 1 + .../src/module.ts | 5 + .../actions/builtin/createBuiltinActions.ts | 2 + 4 files changed, 476 insertions(+) create mode 100644 plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts new file mode 100644 index 0000000000..b5cce9d0a4 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -0,0 +1,468 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { + getBitbucketServerRequestOptions, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { + createTemplateAction, + getRepoSourceDirectory, + commitAndPushBranch, + addFiles, + createBranch as createGitBranch, + cloneRepo, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import fetch, { RequestInit, Response } from 'node-fetch'; +import { Config } from '@backstage/config'; +import fs from 'fs-extra'; +import { getAuthorizationHeader } from './helpers'; + +const createPullRequest = async (opts: { + workspace: string; + repo: string; + title: string; + description?: string; + targetBranch: string; + sourceBranch: string; + authorization: string; + apiBaseUrl: string; +}) => { + const { + workspace, + repo, + title, + description, + targetBranch, + sourceBranch, + authorization, + apiBaseUrl, + } = opts; + + let response: Response; + const data: RequestInit = { + method: 'POST', + body: JSON.stringify({ + title: title, + summary: { + raw: description, + }, + state: 'OPEN', + source: { + branch: { + name: sourceBranch, + }, + }, + destination: { + branch: { + name: targetBranch, + }, + }, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/repositories/${workspace}/${repo}/pullrequests`, + data, + ); + } catch (e) { + throw new Error(`Unable to create pull-reqeusts, ${e}`); + } + + if (response.status !== 201) { + throw new Error( + `Unable to create pull requests, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + return r.links.self.href; +}; + +const findBranches = async (opts: { + project: string; + repo: string; + branchName: string; + authorization: string; + apiBaseUrl: string; +}) => { + const { project, repo, branchName, authorization, apiBaseUrl } = opts; + + let response: Response; + const options: RequestInit = { + method: 'GET', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${encodeURIComponent( + project, + )}/repos/${encodeURIComponent( + repo, + )}/branches?boostMatches=true&filterText=${encodeURIComponent( + branchName, + )}`, + options, + ); + } catch (e) { + throw new Error(`Unable to get branches, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to get branches, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + for (const object of r.values) { + if (object.displayId === branchName) { + return object; + } + } + + return undefined; +}; +const createBranch = async (opts: { + project: string; + repo: string; + branchName: string; + authorization: string; + apiBaseUrl: string; + startPoint: string; +}) => { + const { project, repo, branchName, authorization, apiBaseUrl, startPoint } = + opts; + + let response: Response; + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({ + name: branchName, + startPoint, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${encodeURIComponent( + project, + )}/repos/${encodeURIComponent(repo)}/branches`, + options, + ); + } catch (e) { + throw new Error(`Unable to create branch, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to create branch, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + return await response.json(); +}; +const getDefaultBranch = async (opts: { + workspace: string; + repo: string; + authorization: string; + apiBaseUrl: string; +}): Promise => { + const { workspace, repo, authorization, apiBaseUrl } = opts; + let response: Response; + + const options: RequestInit = { + method: 'GET', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/repositories/${workspace}/${repo}`, + options, + ); + } catch (error) { + throw error; + } + + const { mainbranch } = await response.json(); + console.log(authorization); + const defaultBranch = mainbranch.name; + if (!defaultBranch) { + throw new Error(`Could not fetch default branch for ${workspace}/${repo}`); + } + return defaultBranch; +}; +/** + * Creates a Bitbucket Cloud Pull Request action. + * @public + */ +export function createPublishBitbucketCloudPullRequestAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations, config } = options; + + return createTemplateAction<{ + repoUrl: string; + title: string; + description?: string; + targetBranch?: string; + sourceBranch: string; + token?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; + }>({ + id: 'publish:bitbucketCloud:pull-request', + // examples, + schema: { + input: { + type: 'object', + required: ['repoUrl', 'title', 'sourceBranch'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + title: { + title: 'Pull Request title', + type: 'string', + description: 'The title for the pull request', + }, + description: { + title: 'Pull Request Description', + type: 'string', + description: 'The description of the pull request', + }, + targetBranch: { + title: 'Target Branch', + type: 'string', + description: `Branch of repository to apply changes to. The default value is 'master'`, + }, + sourceBranch: { + title: 'Source Branch', + type: 'string', + description: 'Branch of repository to copy changes from', + }, + token: { + title: 'Authorization Token', + type: 'string', + description: + 'The token to use for authorization to BitBucket Cloud', + }, + gitAuthorName: { + title: 'Author Name', + type: 'string', + description: `Sets the author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Author Email', + type: 'string', + description: `Sets the author email for the commit.`, + }, + }, + }, + output: { + type: 'object', + properties: { + pullRequestUrl: { + title: 'A URL to the pull request with the provider', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + title, + description, + targetBranch, + sourceBranch, + gitAuthorName, + gitAuthorEmail, + } = ctx.input; + + const { project, repo, host, workspace } = parseRepoUrl( + repoUrl, + integrations, + ); + + if (!project) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + ); + } + + 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( + ctx.input.token ? { token: ctx.input.token } : integrationConfig.config, + ); + + const apiBaseUrl = integrationConfig.config.apiBaseUrl; + + let finalTargetBranch = targetBranch; + if (!finalTargetBranch) { + finalTargetBranch = await getDefaultBranch({ + workspace: workspace!, + repo, + authorization, + apiBaseUrl, + }); + } + + // const toRef = await findBranches({ + // project, + // repo, + // branchName: finalTargetBranch!, + // authorization, + // apiBaseUrl, + // }); + + // let fromRef = await findBranches({ + // project, + // repo, + // branchName: sourceBranch, + // authorization, + // apiBaseUrl, + // }); + + // if (!fromRef) { + // // create branch + // ctx.logger.info( + // `source branch not found -> creating branch named: ${sourceBranch} lastCommit: ${toRef.latestCommit}`, + // ); + // const latestCommit = toRef.latestCommit; + + // fromRef = await createBranch({ + // project, + // repo, + // branchName: sourceBranch, + // authorization, + // apiBaseUrl, + // startPoint: latestCommit, + // }); + + // const remoteUrl = `https://${host}/scm/${project}/${repo}.git`; + + // const auth = authConfig.token + // ? { + // token: token!, + // } + // : { + // username: authConfig.username!, + // password: authConfig.password!, + // }; + + // const gitAuthorInfo = { + // name: + // gitAuthorName || + // config.getOptionalString('scaffolder.defaultAuthor.name'), + // email: + // gitAuthorEmail || + // config.getOptionalString('scaffolder.defaultAuthor.email'), + // }; + + // const tempDir = await ctx.createTemporaryDirectory(); + // const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); + // await cloneRepo({ + // url: remoteUrl, + // dir: tempDir, + // auth, + // logger: ctx.logger, + // ref: sourceBranch, + // }); + + // await createGitBranch({ + // dir: tempDir, + // auth, + // logger: ctx.logger, + // ref: sourceBranch, + // }); + + // // copy files + // fs.cpSync(sourceDir, tempDir, { + // recursive: true, + // filter: path => { + // return !(path.indexOf('.git') > -1); + // }, + // }); + + // await addFiles({ + // dir: tempDir, + // auth, + // logger: ctx.logger, + // filepath: '.', + // }); + + // await commitAndPushBranch({ + // dir: tempDir, + // auth, + // logger: ctx.logger, + // commitMessage: + // description ?? + // config.getOptionalString('scaffolder.defaultCommitMessage') ?? + // '', + // gitAuthorInfo, + // branch: sourceBranch, + // }); + // } + + const pullRequestUrl = await createPullRequest({ + workspace: workspace!, + repo, + title, + description, + targetBranch: finalTargetBranch, + sourceBranch, + authorization, + apiBaseUrl, + }); + + ctx.output('pullRequestUrl', pullRequestUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/index.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/index.ts index a9826a268a..c4030c0b52 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/index.ts @@ -15,3 +15,4 @@ */ export * from './bitbucketCloud'; export { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; +export * from './bitbucketCloudPullRequest'; diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts index 5bf49f7cd9..c0f922ca08 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts @@ -24,6 +24,7 @@ import { import { createBitbucketPipelinesRunAction, createPublishBitbucketCloudAction, + createPublishBitbucketCloudPullRequestAction, } from './actions'; import { ScmIntegrations } from '@backstage/integration'; import { handleAutocompleteRequest } from './autocomplete/autocomplete'; @@ -48,6 +49,10 @@ export const bitbucketCloudModule = createBackendModule({ scaffolder.addActions( createPublishBitbucketCloudAction({ integrations, config }), createBitbucketPipelinesRunAction({ integrations }), + createPublishBitbucketCloudPullRequestAction({ + integrations, + config, + }), ); autocomplete.addAutocompleteProvider({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index dba867c96b..e2392150cc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -62,6 +62,7 @@ import { createPublishBitbucketAction } from '@backstage/plugin-scaffolder-backe import { createPublishBitbucketCloudAction, createBitbucketPipelinesRunAction, + createPublishBitbucketCloudPullRequestAction, } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; import { @@ -197,6 +198,7 @@ export const createBuiltinActions = ( integrations, config, }), + createPublishBitbucketCloudPullRequestAction({ integrations, config }), createPublishBitbucketServerAction({ integrations, config, From 7f65deff34dda856ae9c0845981af1bc3616c8cd Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 3 Sep 2024 17:17:54 +0200 Subject: [PATCH 02/14] feat: make the publish:bitbucketCloud:pull-request action work end-to-end Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 191 +++++++++--------- 1 file changed, 92 insertions(+), 99 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index b5cce9d0a4..47261021b6 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -15,10 +15,7 @@ */ import { InputError } from '@backstage/errors'; -import { - getBitbucketServerRequestOptions, - ScmIntegrationRegistry, -} from '@backstage/integration'; +import { ScmIntegrationRegistry } from '@backstage/integration'; import { createTemplateAction, getRepoSourceDirectory, @@ -152,22 +149,30 @@ const findBranches = async (opts: { return undefined; }; const createBranch = async (opts: { - project: string; + workspace: string; repo: string; branchName: string; authorization: string; apiBaseUrl: string; - startPoint: string; + startBranch: string; }) => { - const { project, repo, branchName, authorization, apiBaseUrl, startPoint } = - opts; + const { + workspace, + repo, + branchName, + authorization, + apiBaseUrl, + startBranch, + } = opts; let response: Response; const options: RequestInit = { method: 'POST', body: JSON.stringify({ name: branchName, - startPoint, + target: { + hash: startBranch, + }, }), headers: { Authorization: authorization, @@ -177,16 +182,14 @@ const createBranch = async (opts: { try { response = await fetch( - `${apiBaseUrl}/projects/${encodeURIComponent( - project, - )}/repos/${encodeURIComponent(repo)}/branches`, + `${apiBaseUrl}/repositories/${workspace}/${repo}/refs/branches`, options, ); } catch (e) { throw new Error(`Unable to create branch, ${e}`); } - if (response.status !== 200) { + if (response.status !== 201) { throw new Error( `Unable to create branch, ${response.status} ${ response.statusText @@ -354,102 +357,92 @@ export function createPublishBitbucketCloudPullRequestAction(options: { }); } - // const toRef = await findBranches({ - // project, - // repo, - // branchName: finalTargetBranch!, - // authorization, - // apiBaseUrl, - // }); + await createBranch({ + workspace: workspace!, + repo, + branchName: sourceBranch, + authorization, + apiBaseUrl, + startBranch: finalTargetBranch, + }); - // let fromRef = await findBranches({ - // project, - // repo, - // branchName: sourceBranch, - // authorization, - // apiBaseUrl, - // }); + const remoteUrl = `https://${host}/${workspace}/${repo}.git`; - // if (!fromRef) { - // // create branch - // ctx.logger.info( - // `source branch not found -> creating branch named: ${sourceBranch} lastCommit: ${toRef.latestCommit}`, - // ); - // const latestCommit = toRef.latestCommit; + let auth; - // fromRef = await createBranch({ - // project, - // repo, - // branchName: sourceBranch, - // authorization, - // apiBaseUrl, - // startPoint: latestCommit, - // }); + if (ctx.input.token) { + auth = { + username: 'x-token-auth', + password: ctx.input.token, + }; + } else { + if ( + !integrationConfig.config.username || + !integrationConfig.config.appPassword + ) { + throw new Error( + 'Credentials for Bitbucket Cloud integration required for this action.', + ); + } - // const remoteUrl = `https://${host}/scm/${project}/${repo}.git`; + auth = { + username: integrationConfig.config.username, + password: integrationConfig.config.appPassword, + }; + } - // const auth = authConfig.token - // ? { - // token: token!, - // } - // : { - // username: authConfig.username!, - // password: authConfig.password!, - // }; + const gitAuthorInfo = { + name: + gitAuthorName || + config.getOptionalString('scaffolder.defaultAuthor.name'), + email: + gitAuthorEmail || + config.getOptionalString('scaffolder.defaultAuthor.email'), + }; - // const gitAuthorInfo = { - // name: - // gitAuthorName || - // config.getOptionalString('scaffolder.defaultAuthor.name'), - // email: - // gitAuthorEmail || - // config.getOptionalString('scaffolder.defaultAuthor.email'), - // }; + const tempDir = await ctx.createTemporaryDirectory(); + const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); + await cloneRepo({ + url: remoteUrl, + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); - // const tempDir = await ctx.createTemporaryDirectory(); - // const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); - // await cloneRepo({ - // url: remoteUrl, - // dir: tempDir, - // auth, - // logger: ctx.logger, - // ref: sourceBranch, - // }); + await createGitBranch({ + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); - // await createGitBranch({ - // dir: tempDir, - // auth, - // logger: ctx.logger, - // ref: sourceBranch, - // }); + // copy files + fs.cpSync(sourceDir, tempDir, { + recursive: true, + filter: path => { + return !(path.indexOf('.git') > -1); + }, + }); - // // copy files - // fs.cpSync(sourceDir, tempDir, { - // recursive: true, - // filter: path => { - // return !(path.indexOf('.git') > -1); - // }, - // }); + await addFiles({ + dir: tempDir, + auth, + logger: ctx.logger, + filepath: '.', + }); - // await addFiles({ - // dir: tempDir, - // auth, - // logger: ctx.logger, - // filepath: '.', - // }); - - // await commitAndPushBranch({ - // dir: tempDir, - // auth, - // logger: ctx.logger, - // commitMessage: - // description ?? - // config.getOptionalString('scaffolder.defaultCommitMessage') ?? - // '', - // gitAuthorInfo, - // branch: sourceBranch, - // }); - // } + await commitAndPushBranch({ + dir: tempDir, + auth, + logger: ctx.logger, + commitMessage: + description ?? + config.getOptionalString('scaffolder.defaultCommitMessage') ?? + '', + gitAuthorInfo, + branch: sourceBranch, + }); const pullRequestUrl = await createPullRequest({ workspace: workspace!, From 8a36e52d428805555de59bf29e45ef44e4b17a0f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 09:40:56 +0200 Subject: [PATCH 03/14] fix: validate workspace instead of project Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index 47261021b6..033eb59b6c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -323,12 +323,9 @@ export function createPublishBitbucketCloudPullRequestAction(options: { gitAuthorEmail, } = ctx.input; - const { project, repo, host, workspace } = parseRepoUrl( - repoUrl, - integrations, - ); + const { workspace, repo, host } = parseRepoUrl(repoUrl, integrations); - if (!project) { + if (!workspace) { throw new InputError( `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, ); @@ -350,15 +347,17 @@ export function createPublishBitbucketCloudPullRequestAction(options: { let finalTargetBranch = targetBranch; if (!finalTargetBranch) { finalTargetBranch = await getDefaultBranch({ - workspace: workspace!, + workspace, repo, authorization, apiBaseUrl, }); } + // TODO: check if sourceBranch already exists and just create a pull request for it if so + await createBranch({ - workspace: workspace!, + workspace, repo, branchName: sourceBranch, authorization, @@ -445,7 +444,7 @@ export function createPublishBitbucketCloudPullRequestAction(options: { }); const pullRequestUrl = await createPullRequest({ - workspace: workspace!, + workspace, repo, title, description, From f582198055986e01a1ed30686f330da67cbefc41 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 09:48:02 +0200 Subject: [PATCH 04/14] fix: remove debugging console.log Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index 033eb59b6c..faec1b6da8 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -226,7 +226,6 @@ const getDefaultBranch = async (opts: { } const { mainbranch } = await response.json(); - console.log(authorization); const defaultBranch = mainbranch.name; if (!defaultBranch) { throw new Error(`Could not fetch default branch for ${workspace}/${repo}`); From c53e1769f2929c8baa9d389aebf7d4e7597c8159 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 11:24:03 +0200 Subject: [PATCH 05/14] feat: implement source branch check Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 182 +++++++++--------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index faec1b6da8..71944c18ae 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -99,13 +99,13 @@ const createPullRequest = async (opts: { }; const findBranches = async (opts: { - project: string; + workspace: string; repo: string; branchName: string; authorization: string; apiBaseUrl: string; }) => { - const { project, repo, branchName, authorization, apiBaseUrl } = opts; + const { workspace, repo, branchName, authorization, apiBaseUrl } = opts; let response: Response; const options: RequestInit = { @@ -118,12 +118,8 @@ const findBranches = async (opts: { try { response = await fetch( - `${apiBaseUrl}/projects/${encodeURIComponent( - project, - )}/repos/${encodeURIComponent( - repo, - )}/branches?boostMatches=true&filterText=${encodeURIComponent( - branchName, + `${apiBaseUrl}/repositories/${workspace}/${repo}/refs/branches?q=${encodeURIComponent( + `name = "${branchName}"`, )}`, options, ); @@ -140,13 +136,8 @@ const findBranches = async (opts: { } const r = await response.json(); - for (const object of r.values) { - if (object.displayId === branchName) { - return object; - } - } - return undefined; + return r.values[0]; }; const createBranch = async (opts: { workspace: string; @@ -353,95 +344,108 @@ export function createPublishBitbucketCloudPullRequestAction(options: { }); } - // TODO: check if sourceBranch already exists and just create a pull request for it if so - - await createBranch({ + const sourceBranchRef = await findBranches({ workspace, repo, branchName: sourceBranch, authorization, apiBaseUrl, - startBranch: finalTargetBranch, }); - const remoteUrl = `https://${host}/${workspace}/${repo}.git`; + if (!sourceBranchRef) { + // create branch + ctx.logger.info( + `source branch not found -> creating branch named: ${sourceBranch}`, + ); - let auth; + await createBranch({ + workspace, + repo, + branchName: sourceBranch, + authorization, + apiBaseUrl, + startBranch: finalTargetBranch, + }); - if (ctx.input.token) { - auth = { - username: 'x-token-auth', - password: ctx.input.token, - }; - } else { - if ( - !integrationConfig.config.username || - !integrationConfig.config.appPassword - ) { - throw new Error( - 'Credentials for Bitbucket Cloud integration required for this action.', - ); + const remoteUrl = `https://${host}/${workspace}/${repo}.git`; + + let auth; + + if (ctx.input.token) { + auth = { + username: 'x-token-auth', + password: ctx.input.token, + }; + } else { + if ( + !integrationConfig.config.username || + !integrationConfig.config.appPassword + ) { + throw new Error( + 'Credentials for Bitbucket Cloud integration required for this action.', + ); + } + + auth = { + username: integrationConfig.config.username, + password: integrationConfig.config.appPassword, + }; } - auth = { - username: integrationConfig.config.username, - password: integrationConfig.config.appPassword, + const gitAuthorInfo = { + name: + gitAuthorName || + config.getOptionalString('scaffolder.defaultAuthor.name'), + email: + gitAuthorEmail || + config.getOptionalString('scaffolder.defaultAuthor.email'), }; + + const tempDir = await ctx.createTemporaryDirectory(); + const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); + await cloneRepo({ + url: remoteUrl, + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); + + await createGitBranch({ + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); + + // copy files + fs.cpSync(sourceDir, tempDir, { + recursive: true, + filter: path => { + return !(path.indexOf('.git') > -1); + }, + }); + + await addFiles({ + dir: tempDir, + auth, + logger: ctx.logger, + filepath: '.', + }); + + await commitAndPushBranch({ + dir: tempDir, + auth, + logger: ctx.logger, + commitMessage: + description ?? + config.getOptionalString('scaffolder.defaultCommitMessage') ?? + '', + gitAuthorInfo, + branch: sourceBranch, + }); } - const gitAuthorInfo = { - name: - gitAuthorName || - config.getOptionalString('scaffolder.defaultAuthor.name'), - email: - gitAuthorEmail || - config.getOptionalString('scaffolder.defaultAuthor.email'), - }; - - const tempDir = await ctx.createTemporaryDirectory(); - const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); - await cloneRepo({ - url: remoteUrl, - dir: tempDir, - auth, - logger: ctx.logger, - ref: sourceBranch, - }); - - await createGitBranch({ - dir: tempDir, - auth, - logger: ctx.logger, - ref: sourceBranch, - }); - - // copy files - fs.cpSync(sourceDir, tempDir, { - recursive: true, - filter: path => { - return !(path.indexOf('.git') > -1); - }, - }); - - await addFiles({ - dir: tempDir, - auth, - logger: ctx.logger, - filepath: '.', - }); - - await commitAndPushBranch({ - dir: tempDir, - auth, - logger: ctx.logger, - commitMessage: - description ?? - config.getOptionalString('scaffolder.defaultCommitMessage') ?? - '', - gitAuthorInfo, - branch: sourceBranch, - }); - const pullRequestUrl = await createPullRequest({ workspace, repo, From aa5c1f08a1ccad1b6a1d980c572a66eda67a65a6 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 12:16:04 +0200 Subject: [PATCH 06/14] docs: add examples Signed-off-by: Benjamin Janssens --- .../bitbucketCloudPullRequest.examples.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.ts new file mode 100644 index 0000000000..75d99c30c2 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2024 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: + 'Creating pull request on bitbucket cloud with required fields', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketCloud:pull-request', + id: 'publish-bitbucket-cloud-pull-request-minimal', + name: 'Creating pull request on bitbucket cloud', + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'My pull request', + sourceBranch: 'my-feature-branch', + }, + }, + ], + }), + }, + { + description: + 'Creating pull request on bitbucket cloud with custom descriptions', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketCloud:pull-request', + id: 'publish-bitbucket-cloud-pull-request-minimal', + name: 'Creating pull request on bitbucket cloud', + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'My pull request', + sourceBranch: 'my-feature-branch', + description: 'This is a detailed description of my pull request', + }, + }, + ], + }), + }, + { + description: + 'Creating pull request on bitbucket cloud with different target branch', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketCloud:pull-request', + id: 'publish-bitbucket-cloud-pull-request-target-branch', + name: 'Creating pull request on bitbucket cloud', + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'My pull request', + sourceBranch: 'my-feature-branch', + targetBranch: 'development', + }, + }, + ], + }), + }, + { + description: + 'Creating pull request on bitbucket cloud with authorization token', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketCloud:pull-request', + id: 'publish-bitbucket-cloud-pull-request-minimal', + name: 'Creating pull request on bitbucket cloud', + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'My pull request', + sourceBranch: 'my-feature-branch', + token: 'my-auth-token', + }, + }, + ], + }), + }, + { + description: 'Creating pull request on bitbucket cloud with all fields', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketCloud:pull-request', + id: 'publish-bitbucket-cloud-pull-request-minimal', + name: 'Creating pull request on bitbucket cloud', + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'My pull request', + sourceBranch: 'my-feature-branch', + targetBranch: 'development', + description: 'This is a detailed description of my pull request', + token: 'my-auth-token', + gitAuthorName: 'test-user', + gitAuthorEmail: 'test-user@sample.com', + }, + }, + ], + }), + }, +]; From c7c7b2f19078b4638b6ef938d94f31498adfea17 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 13:55:04 +0200 Subject: [PATCH 07/14] test: add tests for examples Signed-off-by: Benjamin Janssens --- ...bitbucketCloudPullRequest.examples.test.ts | 431 ++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts new file mode 100644 index 0000000000..6d5aaaeab1 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts @@ -0,0 +1,431 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('@backstage/plugin-scaffolder-node', () => { + return { + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +import { createPublishBitbucketCloudPullRequestAction } from './bitbucketCloudPullRequest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import yaml from 'yaml'; +import { examples } from './bitbucketCloudPullRequest.examples'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; + +describe('publish:bitbucketCloud:pull-request', () => { + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'test-user', + appPassword: 'test-password', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketCloudPullRequestAction({ + integrations, + config, + }); + const mockContext = createMockActionContext({ + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'Add Scaffolder actions for Bitbucket Cloud', + description: + 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Cloud', + targetBranch: 'master', + sourceBranch: 'develop', + }, + }); + const responseOfBranches = { + pagelen: 1, + size: 187, + values: [ + { + name: 'issue-9.3/AUI-5343-assistive-class', + links: { + commits: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commits/issue-9.3/AUI-5343-assistive-class', + }, + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/refs/branches/issue-9.3/AUI-5343-assistive-class', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/branch/issue-9.3/AUI-5343-assistive-class', + }, + }, + default_merge_strategy: 'squash', + merge_strategies: ['merge_commit', 'squash', 'fast_forward'], + type: 'branch', + target: { + hash: 'e5d1cde9069fcb9f0af90403a4de2150c125a148', + repository: { + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui', + }, + html: { href: 'https://bitbucket.org/atlassian/aui' }, + avatar: { + href: 'https://bytebucket.org/ravatar/%7B585074de-7b60-4fd1-81ed-e0bc7fafbda5%7D?ts=86317', + }, + }, + type: 'repository', + name: 'aui', + full_name: 'atlassian/aui', + uuid: '{585074de-7b60-4fd1-81ed-e0bc7fafbda5}', + }, + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + comments: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/comments', + }, + patch: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/patch/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/commits/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + diff: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/diff/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + approve: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/approve', + }, + statuses: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/statuses', + }, + }, + author: { + raw: 'Marcin Konopka ', + type: 'author', + user: { + display_name: 'Marcin Konopka', + uuid: '{47cc24f4-2a05-4420-88fe-0417535a110a}', + links: { + self: { + href: 'https://api.bitbucket.org/2.0/users/%7B47cc24f4-2a05-4420-88fe-0417535a110a%7D', + }, + html: { + href: 'https://bitbucket.org/%7B47cc24f4-2a05-4420-88fe-0417535a110a%7D/', + }, + avatar: { + href: 'https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/MK-1.png', + }, + }, + nickname: 'Marcin Konopka', + type: 'user', + account_id: '60113d2b47a9540069f4de03', + }, + }, + parents: [ + { + hash: '87f7fc92b00464ae47b13ef65c91884e4ac9be51', + type: 'commit', + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/87f7fc92b00464ae47b13ef65c91884e4ac9be51', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/commits/87f7fc92b00464ae47b13ef65c91884e4ac9be51', + }, + }, + }, + ], + date: '2021-04-13T13:44:49+00:00', + message: 'wip\n', + type: 'commit', + }, + }, + ], + page: 1, + next: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/refs/branches?pagelen=1&page=2', + }; + const responseOfPullRequests = { + type: '', + links: { + self: { href: '', name: '' }, + html: { href: '', name: '' }, + commits: { href: '', name: '' }, + approve: { href: '', name: '' }, + diff: { href: '', name: '' }, + diffstat: { href: '', name: '' }, + comments: { href: '', name: '' }, + activity: { href: '', name: '' }, + merge: { href: '', name: '' }, + decline: { href: '', name: '' }, + }, + id: 108, + title: '', + rendered: { + title: { raw: '', markup: 'markdown', html: '' }, + description: { raw: '', markup: 'markdown', html: '' }, + reason: { raw: '', markup: 'markdown', html: '' }, + }, + summary: { raw: '', markup: 'markdown', html: '' }, + state: 'OPEN', + author: { type: '' }, + source: { + repository: { type: '' }, + branch: { + name: '', + merge_strategies: ['merge_commit'], + default_merge_strategy: '', + }, + commit: { hash: '' }, + }, + destination: { + repository: { type: '' }, + branch: { + name: '', + merge_strategies: ['merge_commit'], + default_merge_strategy: '', + }, + commit: { hash: '' }, + }, + merge_commit: { hash: '' }, + comment_count: 51, + task_count: 53, + close_source_branch: true, + closed_by: { type: '' }, + reason: '', + created_on: '', + updated_on: '', + reviewers: [{ type: '' }], + participants: [{ type: '' }], + }; + + const server = setupServer(); + registerMswTestHooks(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[0].example).steps[0].input, + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[1].example).steps[0].input, + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Basic ${btoa('test-user:test-password')}`, + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[2].example).steps[0].input, + }, + }); + }); + + it(`should ${examples[3].description}`, async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${yaml.parse(examples[3].example).steps[0].input.token}`, + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${yaml.parse(examples[3].example).steps[0].input.token}`, + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[3].example).steps[0].input, + }, + }); + }); + + it(`should ${examples[4].description}`, async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${yaml.parse(examples[4].example).steps[0].input.token}`, + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${yaml.parse(examples[4].example).steps[0].input.token}`, + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[4].example).steps[0].input, + }, + }); + }); +}); From 55f685fd9f137972065e9889d31be87468d2e4c1 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 14:35:26 +0200 Subject: [PATCH 08/14] test: add more tests Signed-off-by: Benjamin Janssens --- ...bitbucketCloudPullRequest.examples.test.ts | 7 +- .../actions/bitbucketCloudPullRequest.test.ts | 418 ++++++++++++++++++ .../src/actions/bitbucketCloudPullRequest.ts | 2 +- 3 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts index 6d5aaaeab1..14d33b8dba 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 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. @@ -171,7 +171,10 @@ describe('publish:bitbucketCloud:pull-request', () => { const responseOfPullRequests = { type: '', links: { - self: { href: '', name: '' }, + self: { + href: 'https://bitbucket.org/workspace/repo/pull-requests/1', + name: '', + }, html: { href: '', name: '' }, commits: { href: '', name: '' }, approve: { href: '', name: '' }, diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts new file mode 100644 index 0000000000..ef900a57c7 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts @@ -0,0 +1,418 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('@backstage/plugin-scaffolder-node', () => { + return { + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +import { createPublishBitbucketCloudPullRequestAction } from './bitbucketCloudPullRequest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; + +describe('publish:bitbucketCloud:pull-request', () => { + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'test-user', + appPassword: 'test-password', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketCloudPullRequestAction({ + integrations, + config, + }); + const mockContext = createMockActionContext({ + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + title: 'Add Scaffolder actions for Bitbucket Cloud', + description: + 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Cloud', + targetBranch: 'master', + sourceBranch: 'develop', + }, + }); + const responseOfBranches = { + pagelen: 1, + size: 187, + values: [ + { + name: 'issue-9.3/AUI-5343-assistive-class', + links: { + commits: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commits/issue-9.3/AUI-5343-assistive-class', + }, + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/refs/branches/issue-9.3/AUI-5343-assistive-class', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/branch/issue-9.3/AUI-5343-assistive-class', + }, + }, + default_merge_strategy: 'squash', + merge_strategies: ['merge_commit', 'squash', 'fast_forward'], + type: 'branch', + target: { + hash: 'e5d1cde9069fcb9f0af90403a4de2150c125a148', + repository: { + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui', + }, + html: { href: 'https://bitbucket.org/atlassian/aui' }, + avatar: { + href: 'https://bytebucket.org/ravatar/%7B585074de-7b60-4fd1-81ed-e0bc7fafbda5%7D?ts=86317', + }, + }, + type: 'repository', + name: 'aui', + full_name: 'atlassian/aui', + uuid: '{585074de-7b60-4fd1-81ed-e0bc7fafbda5}', + }, + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + comments: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/comments', + }, + patch: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/patch/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/commits/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + diff: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/diff/e5d1cde9069fcb9f0af90403a4de2150c125a148', + }, + approve: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/approve', + }, + statuses: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/e5d1cde9069fcb9f0af90403a4de2150c125a148/statuses', + }, + }, + author: { + raw: 'Marcin Konopka ', + type: 'author', + user: { + display_name: 'Marcin Konopka', + uuid: '{47cc24f4-2a05-4420-88fe-0417535a110a}', + links: { + self: { + href: 'https://api.bitbucket.org/2.0/users/%7B47cc24f4-2a05-4420-88fe-0417535a110a%7D', + }, + html: { + href: 'https://bitbucket.org/%7B47cc24f4-2a05-4420-88fe-0417535a110a%7D/', + }, + avatar: { + href: 'https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/MK-1.png', + }, + }, + nickname: 'Marcin Konopka', + type: 'user', + account_id: '60113d2b47a9540069f4de03', + }, + }, + parents: [ + { + hash: '87f7fc92b00464ae47b13ef65c91884e4ac9be51', + type: 'commit', + links: { + self: { + href: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/87f7fc92b00464ae47b13ef65c91884e4ac9be51', + }, + html: { + href: 'https://bitbucket.org/atlassian/aui/commits/87f7fc92b00464ae47b13ef65c91884e4ac9be51', + }, + }, + }, + ], + date: '2021-04-13T13:44:49+00:00', + message: 'wip\n', + type: 'commit', + }, + }, + ], + page: 1, + next: 'https://api.bitbucket.org/2.0/repositories/atlassian/aui/refs/branches?pagelen=1&page=2', + }; + const responseOfDefaultBranch = { + type: '', + links: { + self: { href: '', name: '' }, + html: { href: '', name: '' }, + avatar: { href: '', name: '' }, + pullrequests: { href: '', name: '' }, + commits: { href: '', name: '' }, + forks: { href: '', name: '' }, + watchers: { href: '', name: '' }, + downloads: { href: '', name: '' }, + clone: [{ href: '', name: '' }], + hooks: { href: '', name: '' }, + }, + uuid: '', + full_name: '', + is_private: true, + scm: 'git', + owner: { type: '' }, + name: '', + description: '', + created_on: '', + updated_on: '', + size: 2154, + language: '', + has_issues: true, + has_wiki: true, + fork_policy: 'allow_forks', + project: { type: '' }, + mainbranch: { type: '' }, + }; + const responseOfPullRequests = { + type: '', + links: { + self: { + href: 'https://bitbucket.org/workspace/repo/pull-requests/1', + name: '', + }, + html: { href: '', name: '' }, + commits: { href: '', name: '' }, + approve: { href: '', name: '' }, + diff: { href: '', name: '' }, + diffstat: { href: '', name: '' }, + comments: { href: '', name: '' }, + activity: { href: '', name: '' }, + merge: { href: '', name: '' }, + decline: { href: '', name: '' }, + }, + id: 108, + title: '', + rendered: { + title: { raw: '', markup: 'markdown', html: '' }, + description: { raw: '', markup: 'markdown', html: '' }, + reason: { raw: '', markup: 'markdown', html: '' }, + }, + summary: { raw: '', markup: 'markdown', html: '' }, + state: 'OPEN', + author: { type: '' }, + source: { + repository: { type: '' }, + branch: { + name: '', + merge_strategies: ['merge_commit'], + default_merge_strategy: '', + }, + commit: { hash: '' }, + }, + destination: { + repository: { type: '' }, + branch: { + name: '', + merge_strategies: ['merge_commit'], + default_merge_strategy: '', + }, + commit: { hash: '' }, + }, + merge_commit: { hash: '' }, + comment_count: 51, + task_count: 53, + close_source_branch: true, + closed_by: { type: '' }, + reason: '', + created_on: '', + updated_on: '', + reviewers: [{ type: '' }], + participants: [{ type: '' }], + }; + const handlers = [ + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfDefaultBranch), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ]; + + const server = setupServer(); + registerMswTestHooks(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?project=project&repo=repo', + }, + }), + ).rejects.toThrow(/missing workspace/); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&project=project', + }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo', + }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('should call the correct APIs with basic auth', async () => { + expect.assertions(2); + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', + ); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + }, + }); + }); + + it('should work if the token is provided through ctx.input', async () => { + expect.assertions(2); + const token = 'user-token'; + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfBranches), + ); + }, + ), + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfPullRequests), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + token: token, + }, + }); + }); + + it('should call outputs with the correct urls', async () => { + server.use(...handlers); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pullRequestUrl', + 'https://bitbucket.org/workspace/repo/pull-requests/1', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index 71944c18ae..d3593dc6f5 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -317,7 +317,7 @@ export function createPublishBitbucketCloudPullRequestAction(options: { if (!workspace) { throw new InputError( - `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, ); } From df9ae9efee5c0101b6320b564a5198de6c263db8 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 14:42:04 +0200 Subject: [PATCH 09/14] chore: add changeset Signed-off-by: Benjamin Janssens --- .changeset/fuzzy-fireants-crash.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fuzzy-fireants-crash.md diff --git a/.changeset/fuzzy-fireants-crash.md b/.changeset/fuzzy-fireants-crash.md new file mode 100644 index 0000000000..1feec6232c --- /dev/null +++ b/.changeset/fuzzy-fireants-crash.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added scaffolder action publish:bitbucketCloud:pull-request From 5794d5c1f86bdde98d712550205954a5ea30a531 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 16:23:14 +0200 Subject: [PATCH 10/14] docs: add action to readme Signed-off-by: Benjamin Janssens --- plugins/scaffolder-backend-module-bitbucket-cloud/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/README.md b/plugins/scaffolder-backend-module-bitbucket-cloud/README.md index 0d616f2670..d5ba306fa2 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/README.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/README.md @@ -7,5 +7,6 @@ _This plugin was created through the Backstage CLI_ ## Actions -- `publish:bitbucketCloud` -- `bitbucket:pipelines:run` +* `publish:bitbucketCloud` +* `bitbucket:pipelines:run` +* `publish:bitbucketCloud:pull-request` From 09229c135a1b01772ec1b27f503959fded3aef68 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 16:37:51 +0200 Subject: [PATCH 11/14] fix: add examples; improve consistency of tests Signed-off-by: Benjamin Janssens --- .../bitbucketCloudPullRequest.examples.test.ts | 12 ++++++------ .../src/actions/bitbucketCloudPullRequest.ts | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts index 14d33b8dba..6b30efaa0b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts @@ -239,7 +239,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(200), @@ -252,7 +252,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(201), @@ -279,7 +279,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(200), @@ -292,7 +292,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(201), @@ -319,7 +319,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(200), @@ -332,7 +332,7 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe( - `Basic ${btoa('test-user:test-password')}`, + 'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=', ); return res( ctx.status(201), diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index d3593dc6f5..f0c352bd68 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -29,6 +29,7 @@ import fetch, { RequestInit, Response } from 'node-fetch'; import { Config } from '@backstage/config'; import fs from 'fs-extra'; import { getAuthorizationHeader } from './helpers'; +import { examples } from './bitbucketCloudPullRequest.examples'; const createPullRequest = async (opts: { workspace: string; @@ -244,7 +245,7 @@ export function createPublishBitbucketCloudPullRequestAction(options: { gitAuthorEmail?: string; }>({ id: 'publish:bitbucketCloud:pull-request', - // examples, + examples, schema: { input: { type: 'object', From c4e37e611c8a0e1b2e2b15d6d19547adb51113a8 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 4 Sep 2024 16:42:13 +0200 Subject: [PATCH 12/14] style: fix linting issues; add api reports Signed-off-by: Benjamin Janssens --- .../README.md | 6 +++--- .../api-report.md | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/README.md b/plugins/scaffolder-backend-module-bitbucket-cloud/README.md index d5ba306fa2..462b97e74f 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/README.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/README.md @@ -7,6 +7,6 @@ _This plugin was created through the Backstage CLI_ ## Actions -* `publish:bitbucketCloud` -* `bitbucket:pipelines:run` -* `publish:bitbucketCloud:pull-request` +- `publish:bitbucketCloud` +- `bitbucket:pipelines:run` +- `publish:bitbucketCloud:pull-request` diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md b/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md index 90a8cbc29f..b853023b07 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md @@ -42,4 +42,22 @@ export function createPublishBitbucketCloudAction(options: { }, JsonObject >; + +// @public +export function createPublishBitbucketCloudPullRequestAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction< + { + repoUrl: string; + title: string; + description?: string | undefined; + targetBranch?: string | undefined; + sourceBranch: string; + token?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + }, + JsonObject +>; ``` From 0ef59d53dc9bfcbf42f295cbe0a41e1bd9182b03 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 9 Sep 2024 15:50:14 +0200 Subject: [PATCH 13/14] fix: use correct link for output Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index f0c352bd68..4d1f0e3a40 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -96,7 +96,7 @@ const createPullRequest = async (opts: { } const r = await response.json(); - return r.links.self.href; + return r.links.html.href; }; const findBranches = async (opts: { From 4713b7c2d2a4b3597d63513a5c7ce9d4b7035d6a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 9 Sep 2024 16:19:14 +0200 Subject: [PATCH 14/14] fix: update tests Signed-off-by: Benjamin Janssens --- .../src/actions/bitbucketCloudPullRequest.examples.test.ts | 4 ++-- .../src/actions/bitbucketCloudPullRequest.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts index 6b30efaa0b..48ecae0944 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.examples.test.ts @@ -171,11 +171,11 @@ describe('publish:bitbucketCloud:pull-request', () => { const responseOfPullRequests = { type: '', links: { - self: { + self: { href: '', name: '' }, + html: { href: 'https://bitbucket.org/workspace/repo/pull-requests/1', name: '', }, - html: { href: '', name: '' }, commits: { href: '', name: '' }, approve: { href: '', name: '' }, diff: { href: '', name: '' }, diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts index ef900a57c7..b4db3f78c6 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts @@ -200,11 +200,11 @@ describe('publish:bitbucketCloud:pull-request', () => { const responseOfPullRequests = { type: '', links: { - self: { + self: { href: '', name: '' }, + html: { href: 'https://bitbucket.org/workspace/repo/pull-requests/1', name: '', }, - html: { href: '', name: '' }, commits: { href: '', name: '' }, approve: { href: '', name: '' }, diff: { href: '', name: '' },