Merge pull request #33028 from dfl-aeb/feat/gitlab-publish-settings-name-owner-username
feat(scaffolder-backend-module-gitlab): add settings.name and ownerUsername to publish:gitlab
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.
|
||||
@@ -203,9 +203,11 @@ 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;
|
||||
|
||||
@@ -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,137 @@ 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();
|
||||
});
|
||||
|
||||
it('should warn and continue when adding ownerUsername as project member fails', 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 }]);
|
||||
mockGitlabClient.ProjectMembers.add.mockRejectedValue(
|
||||
new Error('Forbidden'),
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
ownerUsername: 'target-owner',
|
||||
},
|
||||
};
|
||||
ctx.logger.warn = jest.fn();
|
||||
|
||||
await action.handler(ctx);
|
||||
|
||||
expect(mockGitlabClient.ProjectMembers.add).toHaveBeenCalledWith(
|
||||
123456,
|
||||
50,
|
||||
{ userId: 99999 },
|
||||
);
|
||||
expect(ctx.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to add user'),
|
||||
);
|
||||
expect(ctx.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Forbidden'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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