feat(scaffolder-backend-module-gitlab): add settings.name and ownerUsername to publish:gitlab
- Add `settings.name` input to allow setting a custom human-readable project title that differs from the repository slug (path) at creation time - Add `ownerUsername` input to add an arbitrary GitLab user as project owner (access level 50) after project creation, without requiring that user to be the token holder Both inputs are optional and backward-compatible. Signed-off-by: David Fankhänel <128792991+dfl-aeb@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Added two optional inputs to the `publish:gitlab` action:
|
||||
|
||||
- `settings.name`: set a custom human-readable project title that differs from the repository slug.
|
||||
- `ownerUsername`: add a specific GitLab user as project owner (access level 50) of the newly created repository. Requires a privileged token in the integration configuration.
|
||||
@@ -47,7 +47,7 @@ export const createGitlabIssueAction: (options: {
|
||||
discussionToResolve?: string | undefined;
|
||||
epicId?: number | undefined;
|
||||
labels?: string | undefined;
|
||||
issueType?: 'issue' | 'task' | 'incident' | 'test_case' | undefined;
|
||||
issueType?: 'issue' | 'incident' | 'test_case' | 'task' | undefined;
|
||||
mergeRequestToResolveDiscussionsOf?: number | undefined;
|
||||
milestoneId?: number | undefined;
|
||||
weight?: number | undefined;
|
||||
@@ -132,7 +132,7 @@ export const createGitlabRepoPushAction: (options: {
|
||||
sourcePath?: string | undefined;
|
||||
targetPath?: string | undefined;
|
||||
token?: string | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'create' | 'delete' | undefined;
|
||||
},
|
||||
{
|
||||
projectid: string;
|
||||
@@ -181,13 +181,15 @@ export function createPublishGitlabAction(options: {
|
||||
skipExisting?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
setUserAsOwner?: boolean | undefined;
|
||||
ownerUsername?: string | undefined;
|
||||
topics?: string[] | undefined;
|
||||
settings?:
|
||||
| {
|
||||
name?: string | undefined;
|
||||
visibility?: 'internal' | 'private' | 'public' | undefined;
|
||||
path?: string | undefined;
|
||||
description?: string | undefined;
|
||||
merge_method?: 'merge' | 'rebase_merge' | 'ff' | undefined;
|
||||
merge_method?: 'merge' | 'ff' | 'rebase_merge' | undefined;
|
||||
topics?: string[] | undefined;
|
||||
auto_devops_enabled?: boolean | undefined;
|
||||
only_allow_merge_if_pipeline_succeeds?: boolean | undefined;
|
||||
@@ -249,7 +251,7 @@ export const createPublishGitlabMergeRequestAction: (options: {
|
||||
sourcePath?: string | undefined;
|
||||
targetPath?: string | undefined;
|
||||
token?: string | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'skip' | 'create' | 'delete' | undefined;
|
||||
projectid?: string | undefined;
|
||||
removeSourceBranch?: boolean | undefined;
|
||||
assignee?: string | undefined;
|
||||
@@ -300,7 +302,7 @@ export const editGitlabIssueAction: (options: {
|
||||
discussionLocked?: boolean | undefined;
|
||||
dueDate?: string | undefined;
|
||||
epicId?: number | undefined;
|
||||
issueType?: 'issue' | 'task' | 'incident' | 'test_case' | undefined;
|
||||
issueType?: 'issue' | 'incident' | 'test_case' | 'task' | undefined;
|
||||
labels?: string | undefined;
|
||||
milestoneId?: number | undefined;
|
||||
removeLabels?: string | undefined;
|
||||
|
||||
@@ -221,4 +221,40 @@ export const examples: TemplateExample[] = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a GitLab repository with a custom project title that differs from the repository slug.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:gitlab',
|
||||
name: 'Publish to GitLab',
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=my-project-slug&owner=group_name',
|
||||
settings: {
|
||||
name: 'My Project Title',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a GitLab repository and adds a specific user as project owner.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:gitlab',
|
||||
name: 'Publish to GitLab',
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=project_name&owner=group_name',
|
||||
ownerUsername: 'john.doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -45,6 +45,7 @@ const mockGitlabClient = {
|
||||
Users: {
|
||||
showCurrentUser: jest.fn(),
|
||||
allProjects: jest.fn(),
|
||||
all: jest.fn(),
|
||||
},
|
||||
ProjectMembers: {
|
||||
add: jest.fn(),
|
||||
@@ -220,6 +221,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'bob',
|
||||
path: 'bob',
|
||||
visibility: 'private',
|
||||
});
|
||||
expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled();
|
||||
@@ -240,6 +242,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
ci_config_path: '.gitlab-ci.yml',
|
||||
});
|
||||
@@ -263,6 +266,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 12345,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
ci_config_path: '.gitlab-ci.yml',
|
||||
});
|
||||
@@ -348,6 +352,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
ci_config_path: '.gitlab-ci.yml',
|
||||
});
|
||||
@@ -372,6 +377,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'internal',
|
||||
topics: ['topic1', 'topic2'],
|
||||
ci_config_path: '.gitlab-ci.yml',
|
||||
@@ -398,6 +404,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
});
|
||||
expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(2);
|
||||
@@ -442,6 +449,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
});
|
||||
|
||||
@@ -774,6 +782,7 @@ describe('publish:gitlab', () => {
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'repo',
|
||||
path: 'repo',
|
||||
visibility: 'private',
|
||||
});
|
||||
});
|
||||
@@ -798,4 +807,97 @@ describe('publish:gitlab', () => {
|
||||
`The namespace ${owner} is not found or the user doesn't have permissions to access it`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use settings.name as the project title when provided', async () => {
|
||||
mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 });
|
||||
mockGitlabClient.Namespaces.show.mockResolvedValue({
|
||||
id: 1234,
|
||||
kind: 'group',
|
||||
});
|
||||
mockGitlabClient.Groups.allProjects.mockResolvedValue([]);
|
||||
mockGitlabClient.Projects.create.mockResolvedValue({
|
||||
http_url_to_repo: 'http://mockurl.git',
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=my-project-slug&owner=owner',
|
||||
settings: {
|
||||
name: 'My Project Title',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
|
||||
namespaceId: 1234,
|
||||
name: 'My Project Title',
|
||||
path: 'my-project-slug',
|
||||
visibility: 'private',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add ownerUsername as project owner when provided', async () => {
|
||||
mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 });
|
||||
mockGitlabClient.Namespaces.show.mockResolvedValue({
|
||||
id: 1234,
|
||||
kind: 'group',
|
||||
});
|
||||
mockGitlabClient.Groups.allProjects.mockResolvedValue([]);
|
||||
mockGitlabClient.Projects.create.mockResolvedValue({
|
||||
id: 123456,
|
||||
http_url_to_repo: 'http://mockurl.git',
|
||||
});
|
||||
mockGitlabClient.Users.all.mockResolvedValue([{ id: 99999 }]);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
ownerUsername: 'target-owner',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Users.all).toHaveBeenCalledWith({
|
||||
username: 'target-owner',
|
||||
});
|
||||
expect(mockGitlabClient.ProjectMembers.add).toHaveBeenCalledWith(
|
||||
123456,
|
||||
50,
|
||||
{ userId: 99999 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn and continue when ownerUsername is not found', async () => {
|
||||
mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 });
|
||||
mockGitlabClient.Namespaces.show.mockResolvedValue({
|
||||
id: 1234,
|
||||
kind: 'group',
|
||||
});
|
||||
mockGitlabClient.Groups.allProjects.mockResolvedValue([]);
|
||||
mockGitlabClient.Projects.create.mockResolvedValue({
|
||||
id: 123456,
|
||||
http_url_to_repo: 'http://mockurl.git',
|
||||
});
|
||||
mockGitlabClient.Users.all.mockResolvedValue([]);
|
||||
|
||||
const ctx = {
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
ownerUsername: 'unknown-user',
|
||||
},
|
||||
};
|
||||
ctx.logger.warn = jest.fn();
|
||||
|
||||
await action.handler(ctx);
|
||||
|
||||
expect(mockGitlabClient.Users.all).toHaveBeenCalledWith({
|
||||
username: 'unknown-user',
|
||||
});
|
||||
expect(ctx.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not find GitLab user'),
|
||||
);
|
||||
expect(mockGitlabClient.ProjectMembers.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,6 +112,13 @@ export function createPublishGitlabAction(options: {
|
||||
'Set the token user as owner of the newly created repository. Requires a token authorized to do the edit in the integration configuration for the matching host',
|
||||
})
|
||||
.optional(),
|
||||
ownerUsername: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Username of a GitLab user to add as owner (access level 50) of the newly created project. Requires a privileged token in the integration configuration for the matching host.',
|
||||
})
|
||||
.optional(),
|
||||
topics: z =>
|
||||
z
|
||||
.array(z.string(), {
|
||||
@@ -122,6 +129,12 @@ export function createPublishGitlabAction(options: {
|
||||
settings: z =>
|
||||
z
|
||||
.object({
|
||||
name: z
|
||||
.string({
|
||||
description:
|
||||
'Human-readable project name (title). If not provided, the repository name from repoUrl is used.',
|
||||
})
|
||||
.optional(),
|
||||
path: z
|
||||
.string({
|
||||
description:
|
||||
@@ -323,6 +336,7 @@ export function createPublishGitlabAction(options: {
|
||||
await client.Projects.create({
|
||||
namespaceId: targetNamespaceId,
|
||||
name: repo,
|
||||
path: repo,
|
||||
visibility: repoVisibility,
|
||||
...(topics.length ? { topics } : {}),
|
||||
...(Object.keys(settings).length ? { ...settings } : {}),
|
||||
@@ -343,6 +357,33 @@ export function createPublishGitlabAction(options: {
|
||||
await adminClient.ProjectMembers.add(projectId, 50, { userId });
|
||||
}
|
||||
|
||||
if (ctx.input.ownerUsername && integrationConfig.config.token) {
|
||||
try {
|
||||
const adminClient = new Gitlab({
|
||||
host: integrationConfig.config.baseUrl,
|
||||
token: integrationConfig.config.token,
|
||||
});
|
||||
|
||||
const users = await adminClient.Users.all({
|
||||
username: ctx.input.ownerUsername,
|
||||
});
|
||||
if (users.length > 0) {
|
||||
await adminClient.ProjectMembers.add(projectId, 50, {
|
||||
userId: users[0].id,
|
||||
});
|
||||
} else {
|
||||
ctx.logger.warn(
|
||||
`Could not find GitLab user with username '${ctx.input.ownerUsername}'. Skipping owner assignment.`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
ctx.logger.warn(
|
||||
`Failed to add user '${ctx.input.ownerUsername}' as owner: ${errorMessage}. Proceeding without owner assignment.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const remoteUrl = (http_url_to_repo as string).replace(/\.git$/, '');
|
||||
const repoContentsUrl = `${remoteUrl}/-/blob/${defaultBranch}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user