diff --git a/.changeset/chilled-rocks-rule.md b/.changeset/chilled-rocks-rule.md new file mode 100644 index 0000000000..1f8439462c --- /dev/null +++ b/.changeset/chilled-rocks-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added `bitbucketCloud:branchRestriction:create` to allow users to create bitbucket cloud branch restrictions in templates diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index b9ba34228e..babc74e1ae 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -48,6 +48,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "bitbucket": "^2.12.0", "fs-extra": "^11.2.0", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.test.ts new file mode 100644 index 0000000000..7816580a01 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2025 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 { createBitbucketCloudBranchRestrictionAction } from './bitbucketCloudBranchRestriction'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import yaml from 'yaml'; +import { examples } from './bitbucketCloudBranchRestriction.examples'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { Bitbucket } from 'bitbucket'; + +jest.mock('bitbucket', () => ({ + Bitbucket: jest.fn(), +})); + +describe('bitbucketCloud:branchRestriction:create', () => { + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createBitbucketCloudBranchRestrictionAction({ integrations }); + const mockContext = createMockActionContext({ + input: { + repoUrl: + 'bitbucket.org?workspace=workspace&project=project&repo=repo&project=project', + kind: 'push', + }, + }); + + const mockBranchRestrictionsApi = { + branchrestrictions: { + create: jest.fn().mockResolvedValue({ status: 201, data: {} }), + }, + }; + (Bitbucket as unknown as jest.Mock).mockImplementation( + () => mockBranchRestrictionsApi, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => + mockBranchRestrictionsApi.branchrestrictions.create.mockClear(), + ); + + it(`should restrict push to the main branch, except for two user ids, and the admins group`, async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[0].example).steps[0].input, + }, + }); + + expect( + mockBranchRestrictionsApi.branchrestrictions.create, + ).toHaveBeenCalledWith({ + _body: { + branch_match_kind: 'branching_model', + branch_type: 'development', + users: [ + { + uuid: '{a-b-c-d}', + type: 'user', + }, + { + uuid: '{e-f-g-h}', + type: 'user', + }, + ], + groups: [ + { + slug: 'admins', + type: 'group', + }, + ], + kind: 'push', + value: null, + pattern: undefined, + type: 'branchrestriction', + }, + repo_slug: 'repo', + workspace: 'workspace', + }); + }); + + it('should restrict push to the main branch, except for the admins group', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...yaml.parse(examples[1].example).steps[0].input, + }, + }); + + expect( + mockBranchRestrictionsApi.branchrestrictions.create, + ).toHaveBeenCalledWith({ + _body: { + branch_match_kind: 'branching_model', + branch_type: 'development', + users: [], + groups: [ + { + slug: 'admins', + type: 'group', + }, + ], + kind: 'push', + value: null, + pattern: undefined, + type: 'branchrestriction', + }, + repo_slug: 'repo', + workspace: 'workspace', + }); + }); + + // + it('should require passing builds to merge to branches matching a pattern test-feature/*', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect( + mockBranchRestrictionsApi.branchrestrictions.create, + ).toHaveBeenCalledWith({ + _body: { + branch_match_kind: 'glob', + branch_type: undefined, + users: [], + groups: [], + kind: 'require_passing_builds_to_merge', + value: 1, + pattern: 'test-feature/*', + type: 'branchrestriction', + }, + repo_slug: 'repo', + workspace: 'workspace', + }); + }); + + // + it('should require approvals to merge to branches matching a pattern test-feature/*', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect( + mockBranchRestrictionsApi.branchrestrictions.create, + ).toHaveBeenCalledWith({ + _body: { + branch_match_kind: 'glob', + branch_type: undefined, + users: [], + groups: [], + kind: 'require_approvals_to_merge', + value: 1, + pattern: 'test-feature/*', + type: 'branchrestriction', + }, + repo_slug: 'repo', + workspace: 'workspace', + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.ts new file mode 100644 index 0000000000..bfe24d1e70 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.examples.ts @@ -0,0 +1,115 @@ +/* + * 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. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'restrict push to the main branch, except for alice, bob, and the admins group', + example: yaml.stringify({ + steps: [ + { + id: 'createBranchRestriction', + action: 'bitbucketCloud:branchRestriction:create', + name: 'Create Bitbucket Cloud branch restriction', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + kind: 'push', + users: [ + { + uuid: '{a-b-c-d}', + }, + { + uuid: '{e-f-g-h}', + }, + ], + groups: [ + { + slug: 'admins', + }, + ], + }, + }, + ], + }), + }, + { + description: + 'restrict push to the main branch, except for the admins group', + example: yaml.stringify({ + steps: [ + { + id: 'restrictPushToFeatureBranches', + action: 'bitbucketCloud:branchRestriction:create', + name: 'Create Bitbucket Cloud branch restriction by branch type', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + kind: 'push', + groups: [ + { + slug: 'admins', + }, + ], + }, + }, + ], + }), + }, + { + description: + 'require passing builds to merge to branches matching a pattern test-feature/*', + example: yaml.stringify({ + steps: [ + { + id: 'requirePassingBuildsToMergeByPattern', + action: 'bitbucketCloud:branchRestriction:create', + name: 'Create Bitbucket Cloud require passing build to merge to branches matching a pattern', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + kind: 'require_passing_builds_to_merge', + branchMatchKind: 'glob', + pattern: 'test-feature/*', + }, + }, + ], + }), + }, + { + description: + 'require approvals to merge to branches matching a pattern test-feature/*', + example: yaml.stringify({ + steps: [ + { + id: 'requireApprovalsToMergeByPattern', + action: 'bitbucketCloud:branchRestriction:create', + name: 'Create Bitbucket Cloud require approvals to merge branch restriction', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + kind: 'require_approvals_to_merge', + branchMatchKind: 'glob', + pattern: 'test-feature/*', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.test.ts new file mode 100644 index 0000000000..55ee430091 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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 { getBitbucketClient } from './helpers'; +import { Bitbucket } from 'bitbucket'; + +jest.mock('bitbucket', () => ({ + Bitbucket: jest.fn(), +})); + +describe('bitbucketCloud:branchRestriction:create', () => { + it('getBitbucketClient should return the correct headers with username and password', () => { + expect.assertions(1); + const username = 'username'; + const password = 'password'; + getBitbucketClient({ username: username, appPassword: password }); + expect(Bitbucket).toHaveBeenCalledWith({ + auth: { + username: username, + password: password, + }, + }); + }); + + it('getBitbucketClient should throw if only one of username or password is provided', () => { + expect.assertions(2); + const username = 'username'; + const password = 'password'; + + expect(() => getBitbucketClient({ username })).toThrow(Error); + expect(() => getBitbucketClient({ appPassword: password })).toThrow(Error); + }); +}); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.ts new file mode 100644 index 0000000000..d63ccf1b7c --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudBranchRestriction.ts @@ -0,0 +1,182 @@ +/* + * Copyright 2025 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 { ScmIntegrationRegistry } from '@backstage/integration'; +import { + createTemplateAction, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { InputError } from '@backstage/errors'; +import { getBitbucketClient } from './helpers'; +import * as inputProps from './inputProperties'; +import { examples } from './bitbucketCloudBranchRestriction.examples'; + +const createBitbucketCloudBranchRestriction = async (opts: { + workspace: string; + repo: string; + kind: string; + branchMatchKind?: string; + branchType?: string; + pattern?: string; + value?: number; + users?: { uuid: string; type: string }[]; + groups?: { slug: string; type: string }[]; + authorization: { + token?: string; + username?: string; + appPassword?: string; + }; +}) => { + const { + workspace, + repo, + kind, + branchMatchKind, + branchType, + pattern, + value, + users, + groups, + authorization, + } = opts; + + const bitbucket = getBitbucketClient(authorization); + return await bitbucket.branchrestrictions.create({ + _body: { + groups: groups, + users: users, + branch_match_kind: branchMatchKind, + kind: kind, + type: 'branchrestriction', + value: kind === 'push' ? null : value, + pattern: branchMatchKind === 'glob' ? pattern : undefined, + branch_type: + branchMatchKind === 'branching_model' ? branchType : undefined, + }, + repo_slug: repo, + workspace: workspace, + }); +}; + +/** + * Creates a new action that adds a branch restriction to a Bitbucket Cloud repository. + * @public + */ +export function createBitbucketCloudBranchRestrictionAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + return createTemplateAction<{ + repoUrl: string; + kind: string; + branchMatchKind?: string; + branchType?: string; + pattern?: string; + value?: number; + users?: { uuid: string }[]; + groups?: { slug: string }[]; + token?: string; + }>({ + id: 'bitbucketCloud:branchRestriction:create', + examples, + description: + 'Creates branch restrictions for a Bitbucket Cloud repository.', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'kind'], + properties: { + repoUrl: inputProps.repoUrl, + kind: inputProps.restriction.kind, + branchMatchKind: inputProps.restriction.branchMatchKind, + branchType: inputProps.restriction.branchType, + pattern: inputProps.restriction.pattern, + value: inputProps.restriction.value, + users: inputProps.restriction.users, + groups: inputProps.restriction.groups, + token: inputProps.token, + }, + }, + output: { + type: 'object', + properties: { + json: { + title: 'The response from bitbucket cloud', + type: 'string', + }, + statusCode: { + title: 'The status code of the response', + type: 'number', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + kind, + branchMatchKind = 'branching_model', + branchType = 'development', + pattern = '', + value = 1, + users = [], + groups = [], + token = '', + } = ctx.input; + + const { workspace, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!workspace) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } + + 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 = token ? { token: token } : integrationConfig.config; + + const response = await createBitbucketCloudBranchRestriction({ + workspace: workspace, + repo, + kind: kind, + branchMatchKind: branchMatchKind, + branchType: branchType, + pattern: pattern, + value: value, + users: users.map(user => ({ uuid: user.uuid, type: 'user' })), + groups: groups.map(group => ({ slug: group.slug, type: 'group' })), + authorization, + }); + if (response.data.errors) { + ctx.logger.error( + `Error from Bitbucket Cloud Branch Restrictions: ${JSON.stringify( + response.data.errors, + )}`, + ); + } + ctx.logger.info( + `Response from Bitbucket Cloud: ${JSON.stringify(response)}`, + ); + ctx.output('statusCode', response.status); + ctx.output('json', JSON.stringify(response)); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.ts index efa6fd64f4..1abffea393 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.ts @@ -14,6 +14,32 @@ * limitations under the License. */ +import { Bitbucket } from 'bitbucket'; + +export const getBitbucketClient = (config: { + token?: string; + username?: string; + appPassword?: string; +}) => { + if (config.username && config.appPassword) { + return new Bitbucket({ + auth: { + username: config.username, + password: config.appPassword, + }, + }); + } else if (config.token) { + return new Bitbucket({ + auth: { + token: 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`, + ); +}; + export const getAuthorizationHeader = (config: { username?: string; appPassword?: string; diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts index d37426e982..cff1457463 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +const repoUrl = { + title: 'Repository Location', + description: `Accepts the format 'bitbucket.org?repo=reponame&workspace=workspace&project=project' where 'reponame' is the new repository name`, + type: 'string', +}; + const workspace = { title: 'Workspace', description: `The workspace name`, @@ -152,4 +158,95 @@ const pipelinesRunBody = { }, }; +const restriction = { + kind: { + title: 'kind', + description: 'The kind of restriction.', + type: 'string', + enum: [ + 'push', + 'force', + 'delete', + 'restrict_merges', + 'require_tasks_to_be_completed', + 'require_approvals_to_merge', + 'require_default_reviewer_approvals_to_merge', + 'require_no_changes_requested', + 'require_passing_builds_to_merge', + 'require_commits_behind', + 'reset_pullrequest_approvals_on_change', + 'smart_reset_pullrequest_approvals', + 'reset_pullrequest_changes_requested_on_change', + 'require_all_dependencies_merged', + 'enforce_merge_checks', + 'allow_auto_merge_when_builds_pass', + ], + }, + branchMatchKind: { + title: 'branch_match_kind', + description: 'The branch match kind.', + type: 'string', + enum: ['glob', 'branching_model'], + }, + branchType: { + title: 'branch_type', + description: + 'The branch type. When branchMatchKind is set to branching_model, this field is required.', + type: 'string', + enum: [ + 'feature', + 'bugfix', + 'release', + 'hotfix', + 'development', + 'production', + ], + }, + pattern: { + title: 'pattern', + description: + 'The pattern to match branches against. This field is required when branchMatchKind is set to glob.', + type: 'string', + }, + value: { + title: 'value', + description: + 'The value of the restriction. This field is required when kind is one of require_approvals_to_merge / require_default_reviewer_approvals_to_merge / require_passing_builds_to_merge / require_commits_behind.', + type: 'number', + }, + users: { + title: 'users', + description: + 'Names of users that can bypass the push / restrict_merges restriction kind. For any other kind, this field will be ignored.', + type: 'array', + items: { + type: 'object', + properties: { + uuid: { + title: 'uuid', + description: 'The UUID of the user in the format "{a-b-c-d}".', + type: 'string', + }, + }, + }, + }, + groups: { + title: 'groups', + description: + 'Names of groups that can bypass the push / restrict_merges restriction kind. For any other kind, this field will be ignored.', + type: 'array', + items: { + type: 'object', + properties: { + slug: { + title: 'slug', + description: 'The name of the group.', + type: 'string', + }, + }, + }, + }, +}; + export { workspace, repo_slug, pipelinesRunBody, token }; +export { repoUrl, restriction }; diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts index c0f922ca08..4001c064da 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts @@ -21,6 +21,7 @@ import { scaffolderActionsExtensionPoint, scaffolderAutocompleteExtensionPoint, } from '@backstage/plugin-scaffolder-node/alpha'; +import { createBitbucketCloudBranchRestrictionAction } from './actions/bitbucketCloudBranchRestriction'; import { createBitbucketPipelinesRunAction, createPublishBitbucketCloudAction, @@ -53,6 +54,9 @@ export const bitbucketCloudModule = createBackendModule({ integrations, config, }), + createBitbucketCloudBranchRestrictionAction({ + integrations, + }), ); autocomplete.addAutocompleteProvider({ diff --git a/plugins/scaffolder-backend/sample-templates/azure-devops-demo/python-sample/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/azure-devops-demo/python-sample/catalog-info.yaml new file mode 100644 index 0000000000..a8c7e1fd7f --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/azure-devops-demo/python-sample/catalog-info.yaml @@ -0,0 +1,107 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.name }} + annotations: + backstage.io/techdocs-ref: dir:. +spec: + type: service + owner: ${{ values.owner }} + lifecycle: experimental + providesApis: + - ${{ values.name }}-hello-api + - ${{ values.name }}-hello-name-api + - ${{ values.name }}-isalive-api +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: ${{ values.name }}-hello-api + description: A simple hello world API +spec: + type: openapi + lifecycle: experimental + owner: ${{ values.owner }} + system: devops-infra + definition: | + openapi: "3.0.0" + info: + version: 1.0.0 + title: Hello API + license: + name: MIT + servers: + - url: http://localhost:5000 + paths: + /v1/hello: + get: + responses: + '200': + description: Returns a hello world message + summary: Returns a hello world message + ... +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: ${{ values.name }}-hello-name-api + description: A simple hello world API +spec: + type: openapi + lifecycle: experimental + owner: ${{ values.owner }} + system: devops-infra + definition: | + openapi: "3.0.0" + info: + version: 1.0.0 + title: Hello Name API + license: + name: MIT + servers: + - url: http://localhost:5000 + paths: + /v1/hello/{name}: + parameters: + - name: name + in: path + required: true + description: The name to say hello to + schema: + type: string + get: + responses: + '200': + description: Returns a hello world message + summary: Returns a hello world message + ... +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: ${{ values.name }}-isalive-api + description: A simple health check API +spec: + type: openapi + lifecycle: experimental + owner: ${{ values.owner }} + system: devops-infra + definition: | + openapi: "3.0.0" + info: + version: 1.0.0 + title: IsAlive API + license: + name: MIT + servers: + - url: http://localhost:5000 + paths: + /v1/isalive: + get: + responses: + '200': + description: Confirmation that the service is alive + summary: Health check to be used inside Kubernetes + ... + + diff --git a/yarn.lock b/yarn.lock index e5260d2a63..1ecf059965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7415,6 +7415,7 @@ __metadata: "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" + bitbucket: ^2.12.0 fs-extra: ^11.2.0 msw: ^1.0.0 yaml: ^2.0.0 @@ -23861,10 +23862,10 @@ __metadata: languageName: node linkType: hard -"before-after-hook@npm:^2.2.0": - version: 2.2.2 - resolution: "before-after-hook@npm:2.2.2" - checksum: dc2e1ffe389e5afbef2a46790b1b5a50247ed57aba67649cfa9ec2552d248cc9278f222e72fb5a8ff59bbb39d78fbaa97e7234ead0c6b5e8418b67a8644ce207 +"before-after-hook@npm:^2.1.0, before-after-hook@npm:^2.2.0": + version: 2.2.3 + resolution: "before-after-hook@npm:2.2.3" + checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87 languageName: node linkType: hard @@ -23975,6 +23976,19 @@ __metadata: languageName: node linkType: hard +"bitbucket@npm:^2.12.0": + version: 2.12.0 + resolution: "bitbucket@npm:2.12.0" + dependencies: + before-after-hook: ^2.1.0 + deepmerge: ^4.2.2 + is-plain-object: ^3.0.0 + node-fetch: ^2.6.0 + url-template: ^2.0.8 + checksum: 8b60bbe4430e876bf9029c8759418fc8ea6c5ef1eb3cde43f357db010fbbe040dbce3d07c0509a41de6abd5462862433001b7c84038ed82d6a1b1271e8b67bb5 + languageName: node + linkType: hard + "bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -32669,6 +32683,13 @@ __metadata: languageName: node linkType: hard +"is-plain-object@npm:^3.0.0": + version: 3.0.1 + resolution: "is-plain-object@npm:3.0.1" + checksum: d13fe75db350d4ac669595cdfe0242ae87fcecddf2bca858d2dd443a6ed6eb1f69951fac8c2fa85b16106c6b0d7738fea86c2aca2ecee7fd61de15c1574f2cc5 + languageName: node + linkType: hard + "is-plain-object@npm:^5.0.0": version: 5.0.0 resolution: "is-plain-object@npm:5.0.0"