Merge pull request #2429 from taras/tm/scaffold-repo-visibility

Add the ability to configure repository visibility via config
This commit is contained in:
Patrik Oldsberg
2020-09-16 16:52:22 +02:00
committed by GitHub
7 changed files with 298 additions and 134 deletions
@@ -90,16 +90,19 @@ 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();
// Register default templaters
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
@@ -112,9 +115,17 @@ export default async function createPlugin({ logger }: PluginEnvironment) {
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
// Create GitHub client with your access token from environment variables
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({
@@ -177,8 +188,23 @@ docs on creating private GitHub access tokens is available
Note that the need for private GitHub access tokens will be replaced with GitHub
Apps integration further down the line.
The GitHub access token is passed along using the `GITHUB_ACCESS_TOKEN`
environment variable.
The Github access token is retrieved from environment variables via the config.
The config file needs to specify what environment variable the token is
retrieved from. Your config should have the following objects.
```yaml
scaffolder:
github:
token:
$secret:
env: GITHUB_ACCESS_TOKEN
visibility: public # or 'internal' or 'private'
```
You can configure who can see the new repositories that the scaffolder creates
by specifying `visibility` option. Valid options are `public`, `private` and
`internal`. `internal` options is for GitHub Enterprise clients, which means
public within the organization.
### Running the Backend
+16 -3
View File
@@ -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({
@@ -47,6 +47,13 @@ lighthouse:
auth:
providers: {}
scaffolder:
github:
token:
$secret:
env: GITHUB_ACCESS_TOKEN
visibility: public # or 'internal' or 'private'
catalog:
locations:
# Backstage example components
@@ -7,12 +7,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();
@@ -26,8 +30,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({
+8
View File
@@ -272,6 +272,10 @@ async function createPlugin(pluginName: string, appDir: string) {
async function testAppServe(pluginName: string, appDir: string) {
const startApp = spawnPiped(['yarn', 'start'], {
cwd: appDir,
env: {
...process.env,
GITHUB_ACCESS_TOKEN: 'abc',
},
});
Browser.localhost('localhost', 3000);
@@ -351,6 +355,10 @@ async function testBackendStart(appDir: string, isPostgres: boolean) {
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], {
cwd: appDir,
env: {
...process.env,
GITHUB_ACCESS_TOKEN: 'abc',
},
});
let stdout = '';
@@ -53,14 +53,186 @@ 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<ReposCreateInOrgResponseData>);
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<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'User',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
await publisher.publish({
values: {
storePath: 'blam/test',
owner: 'bob',
},
directory: '/tmp/test',
});
expect(
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
name: 'test',
private: false,
});
});
});
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<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
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 private repository in the organization with visibility set to internal', async () => {
mockGithubClient.repos.createInOrg.mockResolvedValue({
data: {
clone_url: 'mockclone',
@@ -74,6 +246,7 @@ describe('GitHub Publisher', () => {
await publisher.publish({
values: {
isOrg: true,
storePath: 'blam/test',
owner: 'bob',
},
@@ -83,10 +256,20 @@ 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 () => {
describe('private visibility in a user account', () => {
const publisher = new GithubPublisher({
client: new Octokit(),
token: 'abc',
repoVisibility: 'private',
});
it('creates a private repository', async () => {
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'mockclone',
@@ -110,116 +293,8 @@ describe('GitHub Publisher', () => {
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
name: 'test',
private: true,
});
});
});
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<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
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',
);
});
});
});
@@ -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,8 +66,16 @@ export class GithubPublisher implements PublisherBase {
const repoCreationPromise =
user.data.type === 'Organization'
? this.client.repos.createInOrg({ name, org: owner })
: this.client.repos.createForAuthenticatedUser({ name });
? this.client.repos.createInOrg({
name,
org: owner,
private: this.repoVisibility !== 'public',
visibility: this.repoVisibility,
})
: this.client.repos.createForAuthenticatedUser({
name,
private: this.repoVisibility === 'private',
});
const { data } = await repoCreationPromise;
@@ -76,10 +101,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');
},
},
});