Merge pull request #26465 from benjidotsh/scaffolder/bitbucket-cloud-pull-request-action-signed-off-2
feat(scaffolder): port Bitbucket Server PR action to Bitbucket Cloud
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch
|
||||
---
|
||||
|
||||
Added scaffolder action publish:bitbucketCloud:pull-request
|
||||
@@ -9,3 +9,4 @@ _This plugin was created through the Backstage CLI_
|
||||
|
||||
- `publish:bitbucketCloud`
|
||||
- `bitbucket:pipelines:run`
|
||||
- `publish:bitbucketCloud:pull-request`
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
```
|
||||
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* 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 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 <mkonopka@atlassian.com>',
|
||||
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: '<string>',
|
||||
links: {
|
||||
self: { href: '<string>', name: '<string>' },
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo/pull-requests/1',
|
||||
name: '<string>',
|
||||
},
|
||||
commits: { href: '<string>', name: '<string>' },
|
||||
approve: { href: '<string>', name: '<string>' },
|
||||
diff: { href: '<string>', name: '<string>' },
|
||||
diffstat: { href: '<string>', name: '<string>' },
|
||||
comments: { href: '<string>', name: '<string>' },
|
||||
activity: { href: '<string>', name: '<string>' },
|
||||
merge: { href: '<string>', name: '<string>' },
|
||||
decline: { href: '<string>', name: '<string>' },
|
||||
},
|
||||
id: 108,
|
||||
title: '<string>',
|
||||
rendered: {
|
||||
title: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
description: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
reason: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
},
|
||||
summary: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
state: 'OPEN',
|
||||
author: { type: '<string>' },
|
||||
source: {
|
||||
repository: { type: '<string>' },
|
||||
branch: {
|
||||
name: '<string>',
|
||||
merge_strategies: ['merge_commit'],
|
||||
default_merge_strategy: '<string>',
|
||||
},
|
||||
commit: { hash: '<string>' },
|
||||
},
|
||||
destination: {
|
||||
repository: { type: '<string>' },
|
||||
branch: {
|
||||
name: '<string>',
|
||||
merge_strategies: ['merge_commit'],
|
||||
default_merge_strategy: '<string>',
|
||||
},
|
||||
commit: { hash: '<string>' },
|
||||
},
|
||||
merge_commit: { hash: '<string>' },
|
||||
comment_count: 51,
|
||||
task_count: 53,
|
||||
close_source_branch: true,
|
||||
closed_by: { type: '<string>' },
|
||||
reason: '<string>',
|
||||
created_on: '<string>',
|
||||
updated_on: '<string>',
|
||||
reviewers: [{ type: '<string>' }],
|
||||
participants: [{ type: '<string>' }],
|
||||
};
|
||||
|
||||
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 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,
|
||||
...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 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,
|
||||
...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 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,
|
||||
...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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
+123
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
+418
@@ -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 <mkonopka@atlassian.com>',
|
||||
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: '<string>',
|
||||
links: {
|
||||
self: { href: '<string>', name: '<string>' },
|
||||
html: { href: '<string>', name: '<string>' },
|
||||
avatar: { href: '<string>', name: '<string>' },
|
||||
pullrequests: { href: '<string>', name: '<string>' },
|
||||
commits: { href: '<string>', name: '<string>' },
|
||||
forks: { href: '<string>', name: '<string>' },
|
||||
watchers: { href: '<string>', name: '<string>' },
|
||||
downloads: { href: '<string>', name: '<string>' },
|
||||
clone: [{ href: '<string>', name: '<string>' }],
|
||||
hooks: { href: '<string>', name: '<string>' },
|
||||
},
|
||||
uuid: '<string>',
|
||||
full_name: '<string>',
|
||||
is_private: true,
|
||||
scm: 'git',
|
||||
owner: { type: '<string>' },
|
||||
name: '<string>',
|
||||
description: '<string>',
|
||||
created_on: '<string>',
|
||||
updated_on: '<string>',
|
||||
size: 2154,
|
||||
language: '<string>',
|
||||
has_issues: true,
|
||||
has_wiki: true,
|
||||
fork_policy: 'allow_forks',
|
||||
project: { type: '<string>' },
|
||||
mainbranch: { type: '<string>' },
|
||||
};
|
||||
const responseOfPullRequests = {
|
||||
type: '<string>',
|
||||
links: {
|
||||
self: { href: '<string>', name: '<string>' },
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo/pull-requests/1',
|
||||
name: '<string>',
|
||||
},
|
||||
commits: { href: '<string>', name: '<string>' },
|
||||
approve: { href: '<string>', name: '<string>' },
|
||||
diff: { href: '<string>', name: '<string>' },
|
||||
diffstat: { href: '<string>', name: '<string>' },
|
||||
comments: { href: '<string>', name: '<string>' },
|
||||
activity: { href: '<string>', name: '<string>' },
|
||||
merge: { href: '<string>', name: '<string>' },
|
||||
decline: { href: '<string>', name: '<string>' },
|
||||
},
|
||||
id: 108,
|
||||
title: '<string>',
|
||||
rendered: {
|
||||
title: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
description: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
reason: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
},
|
||||
summary: { raw: '<string>', markup: 'markdown', html: '<string>' },
|
||||
state: 'OPEN',
|
||||
author: { type: '<string>' },
|
||||
source: {
|
||||
repository: { type: '<string>' },
|
||||
branch: {
|
||||
name: '<string>',
|
||||
merge_strategies: ['merge_commit'],
|
||||
default_merge_strategy: '<string>',
|
||||
},
|
||||
commit: { hash: '<string>' },
|
||||
},
|
||||
destination: {
|
||||
repository: { type: '<string>' },
|
||||
branch: {
|
||||
name: '<string>',
|
||||
merge_strategies: ['merge_commit'],
|
||||
default_merge_strategy: '<string>',
|
||||
},
|
||||
commit: { hash: '<string>' },
|
||||
},
|
||||
merge_commit: { hash: '<string>' },
|
||||
comment_count: 51,
|
||||
task_count: 53,
|
||||
close_source_branch: true,
|
||||
closed_by: { type: '<string>' },
|
||||
reason: '<string>',
|
||||
created_on: '<string>',
|
||||
updated_on: '<string>',
|
||||
reviewers: [{ type: '<string>' }],
|
||||
participants: [{ type: '<string>' }],
|
||||
};
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* 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 { 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';
|
||||
import { examples } from './bitbucketCloudPullRequest.examples';
|
||||
|
||||
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.html.href;
|
||||
};
|
||||
|
||||
const findBranches = async (opts: {
|
||||
workspace: string;
|
||||
repo: string;
|
||||
branchName: string;
|
||||
authorization: string;
|
||||
apiBaseUrl: string;
|
||||
}) => {
|
||||
const { workspace, 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}/repositories/${workspace}/${repo}/refs/branches?q=${encodeURIComponent(
|
||||
`name = "${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();
|
||||
|
||||
return r.values[0];
|
||||
};
|
||||
const createBranch = async (opts: {
|
||||
workspace: string;
|
||||
repo: string;
|
||||
branchName: string;
|
||||
authorization: string;
|
||||
apiBaseUrl: string;
|
||||
startBranch: string;
|
||||
}) => {
|
||||
const {
|
||||
workspace,
|
||||
repo,
|
||||
branchName,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
startBranch,
|
||||
} = opts;
|
||||
|
||||
let response: Response;
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: branchName,
|
||||
target: {
|
||||
hash: startBranch,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
response = await fetch(
|
||||
`${apiBaseUrl}/repositories/${workspace}/${repo}/refs/branches`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create branch, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status !== 201) {
|
||||
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<string> => {
|
||||
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();
|
||||
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 { 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 = getAuthorizationHeader(
|
||||
ctx.input.token ? { token: ctx.input.token } : integrationConfig.config,
|
||||
);
|
||||
|
||||
const apiBaseUrl = integrationConfig.config.apiBaseUrl;
|
||||
|
||||
let finalTargetBranch = targetBranch;
|
||||
if (!finalTargetBranch) {
|
||||
finalTargetBranch = await getDefaultBranch({
|
||||
workspace,
|
||||
repo,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const sourceBranchRef = await findBranches({
|
||||
workspace,
|
||||
repo,
|
||||
branchName: sourceBranch,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
});
|
||||
|
||||
if (!sourceBranchRef) {
|
||||
// create branch
|
||||
ctx.logger.info(
|
||||
`source branch not found -> creating branch named: ${sourceBranch}`,
|
||||
);
|
||||
|
||||
await createBranch({
|
||||
workspace,
|
||||
repo,
|
||||
branchName: sourceBranch,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
startBranch: finalTargetBranch,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
title,
|
||||
description,
|
||||
targetBranch: finalTargetBranch,
|
||||
sourceBranch,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
});
|
||||
|
||||
ctx.output('pullRequestUrl', pullRequestUrl);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
export * from './bitbucketCloud';
|
||||
export { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
|
||||
export * from './bitbucketCloudPullRequest';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user