From 4c0c0bfd509244ccea0ea1720991a675cb86d03a Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Fri, 11 Sep 2020 09:28:07 -0400 Subject: [PATCH] Add the ability to configure repository visibility via config --- packages/backend/src/plugins/scaffolder.ts | 19 +- .../scaffolder/stages/publish/github.test.ts | 318 ++++++++++-------- .../src/scaffolder/stages/publish/github.ts | 31 +- 3 files changed, 219 insertions(+), 149 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index a9e5f185ec..abbcdae1ae 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -23,12 +23,16 @@ import { GithubPublisher, CreateReactAppTemplater, Templaters, + RepoVisilityOptions, } from '@backstage/plugin-scaffolder-backend'; import { Octokit } from '@octokit/rest'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; -export default async function createPlugin({ logger }: PluginEnvironment) { +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); @@ -42,8 +46,17 @@ export default async function createPlugin({ logger }: PluginEnvironment) { preparers.register('file', filePreparer); preparers.register('github', githubPreparer); - const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); - const publisher = new GithubPublisher({ client: githubClient }); + const githubToken = config.getString('scaffolder.github.token'); + const repoVisibility = config.getString( + 'scaffolder.github.visibility', + ) as RepoVisilityOptions; + + const githubClient = new Octokit({ auth: githubToken }); + const publisher = new GithubPublisher({ + client: githubClient, + token: githubToken, + repoVisibility, + }); const dockerClient = new Docker(); return await createRouter({ 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 718c01438c..dd1152d3d5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -53,14 +53,78 @@ const { }; describe('GitHub Publisher', () => { - const publisher = new GithubPublisher({ client: new Octokit() }); - beforeEach(() => { jest.clearAllMocks(); }); - describe('publish: createRemoteInGithub', () => { - it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { + describe('with public repo visibility', () => { + const publisher = new GithubPublisher({ + client: new Octokit(), + token: 'abc', + repoVisibility: 'public', + }); + + describe('publish: createRemoteInGithub', () => { + it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { + mockGithubClient.repos.createInOrg.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + + await publisher.publish({ + values: { + storePath: 'blam/test', + owner: 'bob', + }, + directory: '/tmp/test', + }); + + expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ + org: 'blam', + name: 'test', + private: false, + visibility: 'public', + }); + }); + + it('should use octokit to create a repo in the authed user if the organisation property is not set', 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', + }, + directory: '/tmp/test', + }); + + expect( + mockGithubClient.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + name: 'test', + }); + }); + }); + + describe('publish: createGitDirectory', () => { + const values = { + isOrg: true, + storePath: 'blam/test', + owner: 'lols', + }; + + const mockDir = '/tmp/test/dir'; + mockGithubClient.repos.createInOrg.mockResolvedValue({ data: { clone_url: 'mockclone', @@ -72,8 +136,116 @@ describe('GitHub Publisher', () => { }, } as OctokitResponse); + it('should call init on the repo with the directory', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Repository.init).toHaveBeenCalledWith(mockDir, 0); + }); + + it('should call refresh index on the index and write the new files', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(mockRepo.refreshIndex).toHaveBeenCalled(); + }); + + it('should call add all files and write', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(mockIndex.addAll).toHaveBeenCalled(); + expect(mockIndex.write).toHaveBeenCalled(); + expect(mockIndex.writeTree).toHaveBeenCalled(); + }); + + it('should create a commit with on head with the right name and commiter', async () => { + const mockSignature = { mockSignature: 'bloblly' }; + Signature.now.mockReturnValue(mockSignature); + + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Signature.now).toHaveBeenCalledTimes(2); + expect(Signature.now).toHaveBeenCalledWith( + 'Scaffolder', + 'scaffolder@backstage.io', + ); + + expect(mockRepo.createCommit).toHaveBeenCalledWith( + 'HEAD', + mockSignature, + mockSignature, + 'initial commit', + 'mockoid', + [], + ); + }); + + it('creates a remote with the repo and remote', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Remote.create).toHaveBeenCalledWith( + mockRepo, + 'origin', + 'mockclone', + ); + }); + + it('shoud push to the remote repo', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + const [remotes, { callbacks }] = mockRemote.push.mock + .calls[0] as NodeGit.PushOptions[]; + + expect(remotes).toEqual(['refs/heads/master:refs/heads/master']); + + callbacks?.credentials?.(); + + expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith( + 'abc', + 'x-oauth-basic', + ); + }); + }); + }); + + describe('with internal repo visibility', () => { + const publisher = new GithubPublisher({ + client: new Octokit(), + token: 'abc', + repoVisibility: 'internal', + }); + + it('creates a repo in an organisation with internal visibility', async () => { + mockGithubClient.repos.createInOrg.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { + type: 'Organization', + }, + } as OctokitResponse); + await publisher.publish({ values: { + isOrg: true, storePath: 'blam/test', owner: 'bob', }, @@ -83,143 +255,9 @@ describe('GitHub Publisher', () => { expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ org: 'blam', name: 'test', + private: true, + visibility: 'internal', }); }); - - it('should use octokit to create a repo in the authed user if the organisation property is not set', 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', - }, - directory: '/tmp/test', - }); - - expect( - mockGithubClient.repos.createForAuthenticatedUser, - ).toHaveBeenCalledWith({ - name: 'test', - }); - }); - }); - - describe('publish: createGitDirectory', () => { - const values = { - isOrg: true, - storePath: 'blam/test', - owner: 'lols', - }; - - const mockDir = '/tmp/test/dir'; - - mockGithubClient.repos.createInOrg.mockResolvedValue({ - data: { - clone_url: 'mockclone', - }, - } as OctokitResponse); - mockGithubClient.users.getByUsername.mockResolvedValue({ - data: { - type: 'Organization', - }, - } as OctokitResponse); - - it('should call init on the repo with the directory', async () => { - await publisher.publish({ - values, - directory: mockDir, - }); - - expect(Repository.init).toHaveBeenCalledWith(mockDir, 0); - }); - - it('should call refresh index on the index and write the new files', async () => { - await publisher.publish({ - values, - directory: mockDir, - }); - - expect(mockRepo.refreshIndex).toHaveBeenCalled(); - }); - - it('should call add all files and write', async () => { - await publisher.publish({ - values, - directory: mockDir, - }); - - expect(mockIndex.addAll).toHaveBeenCalled(); - expect(mockIndex.write).toHaveBeenCalled(); - expect(mockIndex.writeTree).toHaveBeenCalled(); - }); - - it('should create a commit with on head with the right name and commiter', async () => { - const mockSignature = { mockSignature: 'bloblly' }; - Signature.now.mockReturnValue(mockSignature); - - await publisher.publish({ - values, - directory: mockDir, - }); - - expect(Signature.now).toHaveBeenCalledTimes(2); - expect(Signature.now).toHaveBeenCalledWith( - 'Scaffolder', - 'scaffolder@backstage.io', - ); - - expect(mockRepo.createCommit).toHaveBeenCalledWith( - 'HEAD', - mockSignature, - mockSignature, - 'initial commit', - 'mockoid', - [], - ); - }); - - it('creates a remote with the repo and remote', async () => { - await publisher.publish({ - values, - directory: mockDir, - }); - - expect(Remote.create).toHaveBeenCalledWith( - mockRepo, - 'origin', - 'mockclone', - ); - }); - - it('shoud push to the remote repo', async () => { - await publisher.publish({ - values, - directory: mockDir, - }); - - const [remotes, { callbacks }] = mockRemote.push.mock - .calls[0] as NodeGit.PushOptions[]; - - expect(remotes).toEqual(['refs/heads/master:refs/heads/master']); - - process.env.GITHUb_ACCESS_TOKEN = 'blob'; - - callbacks?.credentials?.(); - - expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith( - process.env.GITHUB_ACCESS_TOKEN, - 'x-oauth-basic', - ); - }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index f3306e3662..22a46ddf1b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -21,10 +21,27 @@ import { JsonValue } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { Repository, Remote, Signature, Cred } from 'nodegit'; +export type RepoVisilityOptions = 'private' | 'internal' | 'public'; + +interface GithubPublisherParams { + client: Octokit; + token: string; + repoVisibility: RepoVisilityOptions; +} + export class GithubPublisher implements PublisherBase { private client: Octokit; - constructor({ client }: { client: Octokit }) { + private token: string; + private repoVisibility: RepoVisilityOptions; + + constructor({ + client, + token, + repoVisibility = 'public', + }: GithubPublisherParams) { this.client = client; + this.token = token; + this.repoVisibility = repoVisibility; } async publish({ @@ -49,7 +66,12 @@ export class GithubPublisher implements PublisherBase { const repoCreationPromise = user.data.type === 'Organization' - ? this.client.repos.createInOrg({ name, org: owner }) + ? this.client.repos.createInOrg({ + name, + org: owner, + private: this.repoVisibility !== 'public', + visibility: this.repoVisibility, + }) : this.client.repos.createForAuthenticatedUser({ name }); const { data } = await repoCreationPromise; @@ -76,10 +98,7 @@ export class GithubPublisher implements PublisherBase { await remoteRepo.push(['refs/heads/master:refs/heads/master'], { callbacks: { credentials: () => { - return Cred.userpassPlaintextNew( - process.env.GITHUB_ACCESS_TOKEN as string, - 'x-oauth-basic', - ); + return Cred.userpassPlaintextNew(this.token, 'x-oauth-basic'); }, }, });