diff --git a/.changeset/breezy-socks-cross.md b/.changeset/breezy-socks-cross.md new file mode 100644 index 0000000000..41bc7f9cdc --- /dev/null +++ b/.changeset/breezy-socks-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +fix: Creating a repository in a user namespace would always lead to an error diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts index e255652f15..e7ceaa3ed7 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts @@ -44,6 +44,7 @@ const mockGitlabClient = { }, Users: { showCurrentUser: jest.fn(), + allProjects: jest.fn(), }, ProjectMembers: { add: jest.fn(), @@ -211,7 +212,7 @@ describe('publish:gitlab', () => { expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); - it('should call the correct Gitlab APIs when the owner is an organization', async () => { + it('should call the correct Gitlab APIs when the owner is a group', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); @@ -232,16 +233,18 @@ describe('publish:gitlab', () => { expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); - it('should call the correct Gitlab APIs when the owner is not an organization', async () => { + it('should call the correct Gitlab APIs when the owner is a user', async () => { mockGitlabClient.Namespaces.show.mockResolvedValue({ id: null }); mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Groups.allProjects.mockResolvedValue([]); + mockGitlabClient.Groups.allProjects.mockRejectedValue({}); + mockGitlabClient.Users.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', }); await action.handler(mockContext); + expect(mockGitlabClient.Groups.allProjects).not.toHaveBeenCalled(); expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner'); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespaceId: 12345, @@ -253,9 +256,12 @@ describe('publish:gitlab', () => { expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); - it('should not call the creation Gitlab APIs when the repository already exists', async () => { + it('should not call the creation Gitlab APIs when the repository already exists in a group', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([ { path: 'repo', @@ -278,9 +284,41 @@ describe('publish:gitlab', () => { expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); }); + it('should not call the creation Gitlab APIs when the repository already exists in a user namespace', async () => { + mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 12345, + kind: 'user', + }); + mockGitlabClient.Users.allProjects.mockResolvedValue([ + { + path: 'repo', + http_url_to_repo: 'http://mockurl.git', + }, + { + path: 'repo-name', + http_url_to_repo: 'http://mockurl.git', + }, + ]); + + await action.handler({ + ...mockContext, + input: { ...mockContext.input, skipExisting: true }, + }); + + expect(mockGitlabClient.Groups.allProjects).not.toHaveBeenCalled(); + expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner'); + expect(mockGitlabClient.Projects.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.ProtectedBranches.protect).not.toHaveBeenCalled(); + }); + it('should call the creation Gitlab APIs when the repository does not yet exists', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([ { path: 'repo-name', @@ -305,7 +343,10 @@ describe('publish:gitlab', () => { it('should call the correct Gitlab APIs when using project settings with override of visibility and topics', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -327,7 +368,10 @@ describe('publish:gitlab', () => { it('should call the correct Gitlab APIs for branches and protectd branches when branch settings provided', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ id: 123456, @@ -368,7 +412,10 @@ describe('publish:gitlab', () => { it('should call the correct Gitlab APIs for variables when their configuration is provided', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ id: 123456, @@ -401,7 +448,10 @@ describe('publish:gitlab', () => { it('should call initRepoAndPush with the correct values', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -422,7 +472,10 @@ describe('publish:gitlab', () => { it('should not call initRepoAndPush when sourcePath is false', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -446,7 +499,10 @@ describe('publish:gitlab', () => { it('should call initRepoAndPush with the correct default branch', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -502,7 +558,10 @@ describe('publish:gitlab', () => { }); mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -549,7 +608,10 @@ describe('publish:gitlab', () => { }); mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -570,7 +632,10 @@ describe('publish:gitlab', () => { it('should call output with the remoteUrl and repoContentsUrl and projectId', async () => { mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ http_url_to_repo: 'http://mockurl.git', @@ -610,7 +675,10 @@ describe('publish:gitlab', () => { config: customAuthorConfig, }); - mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); mockGitlabClient.Groups.allProjects.mockResolvedValue([]); mockGitlabClient.Projects.create.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts index b2641e6656..582716a979 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts @@ -379,13 +379,15 @@ export function createPublishGitlabAction(options: { }); let targetNamespaceId; - + let targetNamespaceKind; try { const namespaceResponse = (await client.Namespaces.show(owner)) as { id: number; + kind: string; }; targetNamespaceId = namespaceResponse.id; + targetNamespaceKind = namespaceResponse.kind; } catch (e) { if (e.cause?.response?.status === 404) { throw new InputError( @@ -401,13 +403,16 @@ export function createPublishGitlabAction(options: { if (!targetNamespaceId) { targetNamespaceId = userId; + targetNamespaceKind = 'user'; } - const existingProjects = await client.Groups.allProjects(owner, { - search: repo, - }); + const existingProjects = + targetNamespaceKind === 'user' + ? await client.Users.allProjects(owner, { search: repo }) + : await client.Groups.allProjects(owner, { search: repo }); + const existingProject = existingProjects.find( - searchPathElem => searchPathElem.path === repo, + project => project.path === repo, ); if (!skipExisting || (skipExisting && !existingProject)) {