From ec8c69d9c38665488bc9aafa5e0f5d5efe0a03e1 Mon Sep 17 00:00:00 2001 From: Ilya Katlinski Date: Fri, 15 Sep 2023 10:13:59 +0200 Subject: [PATCH] feat: add publish:gitlab branches configuration support Signed-off-by: Ilya Katlinski --- .../src/scaffolder/actions/builtin/helpers.ts | 4 + .../builtin/publish/gitlab.examples.ts | 27 ++++++ .../actions/builtin/publish/gitlab.test.ts | 87 ++++++++++++++++++- .../actions/builtin/publish/gitlab.ts | 73 +++++++++++++++- 4 files changed, 187 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 5ac92128ca..1ea3a13687 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -251,3 +251,7 @@ export function getGitCommitMessage( export function entityRefToName(name: string): string { return name.replace(/^.*[:/]/g, ''); } + +export function printGitlabError(error: any): string { + return JSON.stringify({ code: error.code, message: error.description }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts index c010042701..a2f5a20745 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts @@ -89,4 +89,31 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Initializes a git repository with branches settings', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + branches: [ + { + name: 'dev', + create: true, + protected: true, + ref: 'master', + }, + { + name: 'master', + protected: true, + }, + ], + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 53b525a6bf..59d05eb226 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -45,6 +45,12 @@ const mockGitlabClient = { ProjectMembers: { add: jest.fn(), }, + Branches: { + create: jest.fn(), + }, + ProtectedBranches: { + protect: jest.fn(), + }, }; jest.mock('@gitbeaker/node', () => ({ Gitlab: class { @@ -87,7 +93,7 @@ describe('publish:gitlab', () => { output: jest.fn(), createTemporaryDirectory: jest.fn(), }; - const mockContextWithOptions = { + const mockContextWithSettings = { input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -104,6 +110,33 @@ describe('publish:gitlab', () => { output: jest.fn(), createTemporaryDirectory: jest.fn(), }; + const mockContextWithBranches = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + repoVisibility: 'private' as const, + branches: [ + { + name: 'dev', + create: true, + ref: 'master', + protect: true, + }, + { + name: 'stage', + create: true, + }, + { + name: 'perf', + protect: true, + }, + ], + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; beforeEach(() => { jest.resetAllMocks(); @@ -166,6 +199,8 @@ describe('publish:gitlab', () => { name: 'bob', visibility: 'private', }); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); it('should call the correct Gitlab APIs when the owner is an organization', async () => { @@ -184,6 +219,8 @@ describe('publish:gitlab', () => { visibility: 'private', ci_config_path: '.gitlab-ci.yml', }); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); it('should call the correct Gitlab APIs when the owner is not an organization', async () => { @@ -202,16 +239,18 @@ describe('publish:gitlab', () => { visibility: 'private', ci_config_path: '.gitlab-ci.yml', }); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); - it('should call the correct Gitlab APIs when using project options with override of visibility and topics', async () => { + it('should call the correct Gitlab APIs when using project settings with override of visibility and topics', async () => { mockGitlabClient.Users.current.mockResolvedValue({ id: 12345 }); mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', }); - await action.handler(mockContextWithOptions); + await action.handler(mockContextWithSettings); expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner'); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ @@ -221,6 +260,48 @@ describe('publish:gitlab', () => { topics: ['topic1', 'topic2'], ci_config_path: '.gitlab-ci.yml', }); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); + }); + + it('should call the correct Gitlab APIs for branches and protectd branches when branch settings provided', async () => { + mockGitlabClient.Users.current.mockResolvedValue({ id: 12345 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + id: 123456, + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler(mockContextWithBranches); + + expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner'); + expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ + namespace_id: 1234, + name: 'repo', + visibility: 'private', + }); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.ProtectedBranches.protect).toHaveBeenCalledTimes(2); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 123456, + 'dev', + 'master', + ); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 123456, + 'stage', + 'master', + ); + + expect(mockGitlabClient.ProtectedBranches.protect).toHaveBeenCalledWith( + 123456, + 'dev', + ); + expect(mockGitlabClient.ProtectedBranches.protect).toHaveBeenCalledWith( + 123456, + 'perf', + ); }); it('should call initRepoAndPush with the correct values', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 302d006e05..68205d05e9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -18,7 +18,7 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; -import { initRepoAndPush } from '../helpers'; +import { initRepoAndPush, printGitlabError } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; import { examples } from './gitlab.examples'; @@ -49,6 +49,12 @@ export function createPublishGitlabAction(options: { /** @deprecated in favour of settings field */ topics?: string[]; settings?: Record; + branches?: Array<{ + name: string; + protect?: boolean; + create?: boolean; + ref?: string; + }>; }>({ id: 'publish:gitlab', description: @@ -122,6 +128,35 @@ export function createPublishGitlabAction(options: { 'Additional project settings, based on https://docs.gitlab.com/ee/api/projects.html#create-project attributes', type: 'object', }, + branches: { + title: 'Project branches settings', + type: 'array', + items: { + type: 'object', + required: ['name'], + properties: { + name: { + title: 'Branch name', + type: 'string', + }, + protect: { + title: 'Should branch be protected', + description: `Will mark branch as protected. The default value is 'false'`, + type: 'boolean', + }, + create: { + title: 'Should branch be created', + description: `If branch does not exist, it will be created from provided ref. The default value is 'false'`, + type: 'boolean', + }, + ref: { + title: 'Branch reference', + description: `Branch reference to create branch from. The default value is 'master'`, + type: 'string', + }, + }, + }, + }, }, }, output: { @@ -157,6 +192,7 @@ export function createPublishGitlabAction(options: { setUserAsOwner = false, topics = [], settings = {}, + branches = [], } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -247,6 +283,41 @@ export function createPublishGitlabAction(options: { gitAuthorInfo, }); + if (branches) { + for (const branch of branches) { + const { + name, + protect = false, + create = false, + ref = 'master', + } = branch; + + if (create) { + try { + await client.Branches.create(projectId, name, ref); + } catch (e) { + throw new InputError( + `Branch creation failed for ${name}. ${printGitlabError(e)}`, + ); + } + ctx.logger.info( + `Branch ${name} created for ${projectId} with ref ${ref}`, + ); + } + + if (protect) { + try { + await client.ProtectedBranches.protect(projectId, name); + } catch (e) { + throw new InputError( + `Branch protection failed for ${name}. ${printGitlabError(e)}`, + ); + } + ctx.logger.info(`Branch ${name} protected for ${projectId}`); + } + } + } + ctx.output('commitHash', commitResult?.commitHash); ctx.output('remoteUrl', remoteUrl); ctx.output('repoContentsUrl', repoContentsUrl);