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:
@@ -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.
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
+2
-1
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user