Merge pull request #2429 from taras/tm/scaffold-repo-visibility
Add the ability to configure repository visibility via config
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+16
-3
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user