diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index d3c6aaaa4e..7a8a3bc3dc 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -40,8 +40,10 @@ internally. After filling in these variables, you'll get some more fields to fill out which are required for backstage usage: the owner, (which is a `user` in the backstage -system), the `storePath` (which right now must be a GitHub Organisation), and a -non-existing github repository name in the format `organisation/reponame`. +system), the `storePath` (which right now must be a GitHub Organisation or +GitHub user), a non-existing github repository name in the format +`organisation/reponame`, and a GitHub team or user account which should be +granted admin access to the repository. ![Enter backstage vars](../../assets/software-templates/template-picked-2.png) diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts index a302bc5931..e0e9efa479 100644 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts @@ -18,10 +18,14 @@ export const mockGithubClient = { repos: { createInOrg: jest.fn(), createForAuthenticatedUser: jest.fn(), + addCollaborator: jest.fn(), }, users: { getByUsername: jest.fn(), }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, }; export class Octokit { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 5411602a21..c06a553771 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -30,6 +30,7 @@ const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { repos: jest.Mocked; users: jest.Mocked; + teams: jest.Mocked; }; }; @@ -76,6 +77,7 @@ describe('GitHub Publisher', () => { values: { storePath: 'blam/test', owner: 'bob', + access: 'blam/team', }, directory: '/tmp/test', }); @@ -86,6 +88,15 @@ describe('GitHub Publisher', () => { private: false, visibility: 'public', }); + expect( + mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg, + ).toHaveBeenCalledWith({ + org: 'blam', + team_slug: 'team', + owner: 'blam', + repo: 'test', + permission: 'admin', + }); }); it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => { @@ -104,6 +115,7 @@ describe('GitHub Publisher', () => { values: { storePath: 'blam/test', owner: 'bob', + access: 'blam', }, directory: '/tmp/test', }); @@ -114,14 +126,50 @@ describe('GitHub Publisher', () => { name: 'test', private: false, }); + expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled(); + }); + }); + + it('should invite other user in the authed user', async () => { + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { + type: 'User', + }, + } as OctokitResponse); + + await publisher.publish({ + values: { + storePath: 'blam/test', + owner: 'bob', + access: 'bob', + }, + directory: '/tmp/test', + }); + + expect( + mockGithubClient.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + name: 'test', + private: false, + }); + expect(mockGithubClient.repos.addCollaborator).toHaveBeenCalledWith({ + owner: 'blam', + repo: 'test', + username: 'bob', + permission: 'admin', }); }); describe('publish: createGitDirectory', () => { const values = { - isOrg: true, storePath: 'blam/test', owner: 'lols', + access: 'lols', }; const mockDir = '/tmp/test/dir'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index a8f3eaaa02..1d5c9e1947 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -79,6 +79,26 @@ export class GithubPublisher implements PublisherBase { const { data } = await repoCreationPromise; + const access = values.access as string; + if (access?.startsWith(`${owner}/`)) { + const [, team] = access.split('/'); + await this.client.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: team, + owner, + repo: name, + permission: 'admin', + }); + // no need to add access if it's the person who own's the personal account + } else if (access && access !== owner) { + await this.client.repos.addCollaborator({ + owner, + repo: name, + username: access, + permission: 'admin', + }); + } + return data?.clone_url; } diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 1ed0504894..e68c795bd6 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -67,6 +67,11 @@ const OWNER_REPO_SCHEMA = { title: 'Store path', description: 'GitHub store path in org/repo format', }, + access: { + type: 'string' as const, + title: 'Access', + description: 'Who should have access, in org/team or user format', + }, }, };