diff --git a/.changeset/old-onions-hear.md b/.changeset/old-onions-hear.md new file mode 100644 index 0000000000..82dcc09190 --- /dev/null +++ b/.changeset/old-onions-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Update GitLab Merge Request Action to allow source branch to be deleted diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 707b8e62ba..b0f831fc8c 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -345,6 +345,7 @@ export const createPublishGitlabMergeRequestAction: (options: { targetPath: string; token?: string | undefined; projectid?: string | undefined; + removeSourceBranch?: boolean | undefined; }>; // @public diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts new file mode 100644 index 0000000000..987e360617 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -0,0 +1,207 @@ +/* + * Copyright 2022 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 { getRootLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { Writable } from 'stream'; +import { TemplateAction } from '../../types'; +import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; + +const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; +const workspacePath = resolvePath(root, 'my-workspace'); + +const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Branches: { + create: jest.fn(), + }, + Commits: { + create: jest.fn(), + }, + MergeRequests: { + create: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + Projects: { + create: jest.fn(), + show: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + Users: { + current: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('createGitLabMergeRequest', () => { + let instance: TemplateAction; + + beforeEach(() => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + instance = createPublishGitlabMergeRequestAction({ integrations }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe('createGitLabMergeRequestWithoutRemoveBranch', () => { + it('removeSourceBranch is false by default when not passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This MR is really good', + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { description: 'This MR is really good', removeSourceBranch: false }, + ); + }); + }); + + describe('createGitLabMergeRequestWithRemoveBranch', () => { + it('removeSourceBranch is true when true is passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + removeSourceBranch: true, + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { description: 'MR description', removeSourceBranch: true }, + ); + }); + + it('removeSourceBranch is false when false is passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'other MR description', + removeSourceBranch: false, + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'other MR description', + removeSourceBranch: false, + }, + ); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index f717a7a0c6..b1d5b35fa3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -42,6 +42,7 @@ export const createPublishGitlabMergeRequestAction = (options: { token?: string; /** @deprecated Use projectPath instead */ projectid?: string; + removeSourceBranch?: boolean; }>({ id: 'publish:gitlab:merge-request', schema: { @@ -85,6 +86,12 @@ export const createPublishGitlabMergeRequestAction = (options: { type: 'string', description: 'The token to use for authorization to GitLab', }, + removeSourceBranch: { + title: 'Delete source branch', + type: 'boolean', + description: + 'Option to delete source branch once the MR has been merged. Default: false', + }, }, }, output: { @@ -187,7 +194,12 @@ export const createPublishGitlabMergeRequestAction = (options: { destinationBranch, String(defaultBranch), ctx.input.title, - { description: ctx.input.description }, + { + description: ctx.input.description, + removeSourceBranch: ctx.input.removeSourceBranch + ? ctx.input.removeSourceBranch + : false, + }, ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; });