Merge pull request #5205 from anderoo/anderoo/github-permissions

allow collaborators on github creation
This commit is contained in:
Patrik Oldsberg
2021-04-08 18:02:49 +02:00
committed by GitHub
5 changed files with 187 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Adds a collaborator field to GitHub publish action for multiple users and access levels
+1
View File
@@ -55,6 +55,7 @@
"handlebars": "^4.7.6",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"jest-when": "^3.1.0",
"jsonschema": "^1.2.6",
"knex": "^0.95.1",
"lodash": "^4.17.21",
@@ -19,6 +19,7 @@ spec:
- component_id
- use_typescript
- description
- collaborators
properties:
component_id:
title: Name
@@ -39,4 +40,28 @@ spec:
type: boolean
description: Use Github Actions
default: true
collaborators:
title: Collaborators
description: Provide users with permissions
type: array
ui:options:
orderable: false
items:
type: object
required:
- username
- access
properties:
access:
type: string
description: The type of access for the user
default: pull
enum:
- push
- pull
- admin
- maintain
- triage
username:
type: string
description: The username or group
@@ -22,6 +22,7 @@ import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { PassThrough } from 'stream';
import { initRepoAndPush } from '../../../stages/publish/helpers';
import { when } from 'jest-when';
describe('publish:github', () => {
const integrations = ScmIntegrations.fromConfig(
@@ -232,6 +233,114 @@ describe('publish:github', () => {
});
});
it('should add multiple collaborators when provided', async () => {
mockGithubClient.users.getByUsername.mockResolvedValue({
data: { type: 'User' },
});
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'https://github.com/clone/url.git',
html_url: 'https://github.com/html/url',
},
});
await action.handler({
...mockContext,
input: {
...mockContext.input,
collaborators: [
{
access: 'pull',
username: 'robot-1',
},
{
access: 'push',
username: 'robot-2',
},
],
},
});
const commonProperties = {
org: 'owner',
owner: 'owner',
repo: 'repo',
};
expect(
mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1],
).toEqual([
{
...commonProperties,
team_slug: 'robot-1',
permission: 'pull',
},
]);
expect(
mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2],
).toEqual([
{
...commonProperties,
team_slug: 'robot-2',
permission: 'push',
},
]);
});
it('should ignore failures when adding multiple collaborators', async () => {
mockGithubClient.users.getByUsername.mockResolvedValue({
data: { type: 'User' },
});
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'https://github.com/clone/url.git',
html_url: 'https://github.com/html/url',
},
});
when(mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg)
.calledWith({
org: 'owner',
owner: 'owner',
repo: 'repo',
team_slug: 'robot-1',
permission: 'pull',
})
.mockRejectedValueOnce(new Error('Something bad happened') as never);
await action.handler({
...mockContext,
input: {
...mockContext.input,
collaborators: [
{
access: 'pull',
username: 'robot-1',
},
{
access: 'push',
username: 'robot-2',
},
],
},
});
expect(
mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2],
).toEqual([
{
org: 'owner',
owner: 'owner',
repo: 'repo',
team_slug: 'robot-2',
permission: 'push',
},
]);
});
it('should call output with the remoteUrl and the repoContentsUrl', async () => {
mockGithubClient.users.getByUsername.mockResolvedValue({
data: { type: 'User' },
@@ -23,6 +23,9 @@ import { initRepoAndPush } from '../../../stages/publish/helpers';
import { getRepoSourceDirectory, parseRepoUrl } from './util';
import { createTemplateAction } from '../../createTemplateAction';
type Permission = 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
type Collaborator = { access: Permission; username: string };
export function createPublishGithubAction(options: {
integrations: ScmIntegrationRegistry;
}) {
@@ -41,6 +44,7 @@ export function createPublishGithubAction(options: {
access?: string;
sourcePath?: string;
repoVisibility: 'private' | 'internal' | 'public';
collaborators: Collaborator[];
}>({
id: 'publish:github',
description:
@@ -72,6 +76,26 @@ export function createPublishGithubAction(options: {
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the respository.',
type: 'string',
},
collaborators: {
title: 'Collaborators',
description: 'Provide users with permissions',
type: 'array',
items: {
type: 'object',
required: ['username', 'access'],
properties: {
access: {
type: 'string',
description: 'The type of access for the user',
enum: ['push', 'pull', 'admin', 'maintain', 'triage'],
},
username: {
type: 'string',
description: 'The username or group',
},
},
},
},
},
},
output: {
@@ -94,6 +118,7 @@ export function createPublishGithubAction(options: {
description,
access,
repoVisibility = 'private',
collaborators,
} = ctx.input;
const { owner, repo, host } = parseRepoUrl(repoUrl);
@@ -165,6 +190,27 @@ export function createPublishGithubAction(options: {
});
}
if (collaborators) {
for (const {
access: permission,
username: team_slug,
} of collaborators) {
try {
await client.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug,
owner,
repo,
permission,
});
} catch (e) {
ctx.logger.warn(
`Skipping ${permission} access for ${team_slug}, ${e.message}`,
);
}
}
}
const remoteUrl = data.clone_url;
const repoContentsUrl = `${data.html_url}/blob/master`;