Merge pull request #19959 from ikatlinsky/feat/allow-gitlab-push-action-dynamic-parameters

feat: add publish:gitlab project settings support
This commit is contained in:
Ben Lambert
2023-10-10 13:07:51 +02:00
committed by GitHub
6 changed files with 514 additions and 2 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Updated `publish:gitlab` action properties to support additional Gitlab project settings:
- general project settings provided by gitlab project create API (new `settings` property)
- branch level settings to create additional branches and make them protected (new `branches` property)
- project level environment variables settings (new `projectVariables` property)
Marked existed properties `repoVisibility` and `topics` as deprecated, as they are covered by `settings` property.
+30
View File
@@ -679,6 +679,36 @@ export function createPublishGitlabAction(options: {
gitAuthorEmail?: string | undefined;
setUserAsOwner?: boolean | undefined;
topics?: string[] | undefined;
settings?:
| {
path?: string | undefined;
auto_devops_enabled?: boolean | undefined;
ci_config_path?: string | undefined;
description?: string | undefined;
topics?: string[] | undefined;
visibility?: 'internal' | 'private' | 'public' | undefined;
}
| undefined;
branches?:
| {
name: string;
protect?: boolean | undefined;
create?: boolean | undefined;
ref?: string | undefined;
}[]
| undefined;
projectVariables?:
| {
key: string;
value: string;
description?: string | undefined;
variable_type?: string | undefined;
protected?: boolean | undefined;
masked?: boolean | undefined;
raw?: boolean | undefined;
environment_scope?: string | undefined;
}[]
| undefined;
},
JsonObject
>;
@@ -53,6 +53,7 @@ describe('debug:wait examples', () => {
const start = new Date().getTime();
await action.handler(context);
const end = new Date().getTime();
expect(end - start).toBeGreaterThanOrEqual(50);
expect(end - start).toBeGreaterThanOrEqual(45);
expect(end - start).toBeLessThanOrEqual(55);
});
});
@@ -68,4 +68,79 @@ export const examples: TemplateExample[] = [
],
}),
},
{
description: 'Initializes a git repository with additional settings.',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitlab',
name: 'Publish to GitLab',
input: {
repoUrl: 'gitlab.com?repo=project_name&owner=group_name',
settings: {
ci_config_path: '.gitlab-ci.yml',
visibility: 'public',
},
},
},
],
}),
},
{
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,
},
],
},
},
],
}),
},
{
description: 'Initializes a git repository with environment variables',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitlab',
name: 'Publish to GitLab',
input: {
repoUrl: 'gitlab.com?repo=project_name&owner=group_name',
projectVariables: [
{
key: 'key1',
value: 'value1',
protected: true,
masked: false,
},
{
key: 'key2',
value: 'value2',
protected: true,
masked: false,
},
],
},
},
],
}),
},
];
@@ -45,6 +45,15 @@ const mockGitlabClient = {
ProjectMembers: {
add: jest.fn(),
},
Branches: {
create: jest.fn(),
},
ProtectedBranches: {
protect: jest.fn(),
},
ProjectVariables: {
create: jest.fn(),
},
};
jest.mock('@gitbeaker/node', () => ({
Gitlab: class {
@@ -77,6 +86,73 @@ describe('publish:gitlab', () => {
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
repoVisibility: 'private' as const,
settings: {
ci_config_path: '.gitlab-ci.yml',
},
},
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
const mockContextWithSettings = {
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
repoVisibility: 'private' as const,
topics: ['topic'],
settings: {
ci_config_path: '.gitlab-ci.yml',
visibility: 'internal' as const,
topics: ['topic1', 'topic2'],
},
},
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
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(),
};
const mockContextWithVariables = {
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
repoVisibility: 'private' as const,
projectVariables: [
{
key: 'key',
value: 'value',
description: 'description',
protected: true,
masked: true,
},
],
},
workspacePath: 'lol',
logger: getVoidLogger(),
@@ -146,6 +222,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 () => {
@@ -162,7 +240,10 @@ describe('publish:gitlab', () => {
namespace_id: 1234,
name: 'repo',
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 () => {
@@ -179,7 +260,103 @@ describe('publish:gitlab', () => {
namespace_id: 12345,
name: 'repo',
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 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(mockContextWithSettings);
expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner');
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
namespace_id: 1234,
name: 'repo',
visibility: 'internal',
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 the correct Gitlab APIs for variables when their configuration is 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(mockContextWithVariables);
expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner');
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
namespace_id: 1234,
name: 'repo',
visibility: 'private',
});
expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith(
123456,
{
key: 'key',
value: 'value',
description: 'description',
variable_type: 'env_var',
protected: true,
masked: true,
raw: false,
environment_scope: '*',
},
);
});
it('should call initRepoAndPush with the correct values', async () => {
@@ -38,6 +38,7 @@ export function createPublishGitlabAction(options: {
return createTemplateAction<{
repoUrl: string;
defaultBranch?: string;
/** @deprecated in favour of settings.visibility field */
repoVisibility?: 'private' | 'internal' | 'public';
sourcePath?: string;
token?: string;
@@ -45,7 +46,32 @@ export function createPublishGitlabAction(options: {
gitAuthorName?: string;
gitAuthorEmail?: string;
setUserAsOwner?: boolean;
/** @deprecated in favour of settings.topics field */
topics?: string[];
settings?: {
path?: string;
auto_devops_enabled?: boolean;
ci_config_path?: string;
description?: string;
topics?: string[];
visibility?: 'private' | 'internal' | 'public';
};
branches?: Array<{
name: string;
protect?: boolean;
create?: boolean;
ref?: string;
}>;
projectVariables?: Array<{
key: string;
value: string;
description?: string;
variable_type?: string;
protected?: boolean;
masked?: boolean;
raw?: boolean;
environment_scope?: string;
}>;
}>({
id: 'publish:gitlab',
description:
@@ -63,6 +89,7 @@ export function createPublishGitlabAction(options: {
},
repoVisibility: {
title: 'Repository Visibility',
description: `Sets the visibility of the repository. The default value is 'private'. (deprecated, use settings.visibility instead)`,
type: 'string',
enum: ['private', 'public', 'internal'],
},
@@ -105,12 +132,135 @@ export function createPublishGitlabAction(options: {
},
topics: {
title: 'Topic labels',
description: 'Topic labels to apply on the repository.',
description:
'Topic labels to apply on the repository. (deprecated, use settings.topics instead)',
type: 'array',
items: {
type: 'string',
},
},
settings: {
title: 'Project settings',
description:
'Additional project settings, based on https://docs.gitlab.com/ee/api/projects.html#create-project attributes',
type: 'object',
properties: {
path: {
title: 'Project path',
description:
'Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes).',
type: 'string',
},
auto_devops_enabled: {
title: 'Auto DevOps enabled',
description: 'Enable Auto DevOps for this project',
type: 'boolean',
},
ci_config_path: {
title: 'CI config path',
description: 'Custom CI config path for this project',
type: 'string',
},
description: {
title: 'Project description',
description: 'Short project description',
type: 'string',
},
topics: {
title: 'Topic labels',
description: 'Topic labels to apply on the repository',
type: 'array',
items: {
type: 'string',
},
},
visibility: {
title: 'Project visibility',
description:
'The visibility of the project. Can be private, internal, or public. The default value is private.',
type: 'string',
enum: ['private', 'public', 'internal'],
},
},
},
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',
},
},
},
},
projectVariables: {
title: 'Project variables',
description:
'Project variables settings based on Gitlab Project Environments API - https://docs.gitlab.com/ee/api/project_level_variables.html#create-a-variable',
type: 'array',
items: {
type: 'object',
required: ['key', 'value'],
properties: {
key: {
title: 'Variable key',
description:
'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed',
type: 'string',
},
value: {
title: 'Variable value',
description: 'The value of a variable',
type: 'string',
},
description: {
title: 'Variable description',
description: `The description of the variable. The default value is 'null'`,
type: 'string',
},
variable_type: {
title: 'Variable type',
description: `The type of a variable. The default value is 'env_var'`,
type: 'string',
enum: ['env_var', 'file'],
},
protected: {
title: 'Variable protection',
description: `Whether the variable is protected. The default value is 'false'`,
type: 'boolean',
},
raw: {
title: 'Variable raw',
description: `Whether the variable is in raw format. The default value is 'false'`,
type: 'boolean',
},
environment_scope: {
title: 'Variable environment scope',
description: `The environment_scope of the variable. The default value is '*'`,
type: 'string',
},
},
},
},
},
},
output: {
@@ -145,6 +295,9 @@ export function createPublishGitlabAction(options: {
gitAuthorEmail,
setUserAsOwner = false,
topics = [],
settings = {},
branches = [],
projectVariables = [],
} = ctx.input;
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
@@ -204,6 +357,7 @@ export function createPublishGitlabAction(options: {
name: repo,
visibility: repoVisibility,
...(topics.length ? { topics } : {}),
...(Object.keys(settings).length ? { ...settings } : {}),
});
// When setUserAsOwner is true the input token is expected to come from an unprivileged user GitLab
@@ -247,6 +401,66 @@ 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}`);
}
}
}
if (projectVariables) {
for (const variable of projectVariables) {
const variableWithDefaults = Object.assign(variable, {
variable_type: variable.variable_type ?? 'env_var',
protected: variable.protected ?? false,
masked: variable.masked ?? false,
raw: variable.raw ?? false,
environment_scope: variable.environment_scope ?? '*',
});
try {
await client.ProjectVariables.create(
projectId,
variableWithDefaults,
);
} catch (e) {
throw new InputError(
`Environment variable creation failed for ${
variableWithDefaults.key
}. ${printGitlabError(e)}`,
);
}
}
}
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
@@ -254,3 +468,7 @@ export function createPublishGitlabAction(options: {
},
});
}
function printGitlabError(error: any): string {
return JSON.stringify({ code: error.code, message: error.description });
}