feat: add publish:gitlab branches configuration support

Signed-off-by: Ilya Katlinski <ilya.katlinsky@gmail.com>
This commit is contained in:
Ilya Katlinski
2023-09-15 10:13:59 +02:00
parent 768d521c79
commit ec8c69d9c3
4 changed files with 187 additions and 4 deletions
@@ -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 });
}
@@ -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,
},
],
},
},
],
}),
},
];
@@ -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 () => {
@@ -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<string, any>;
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);