Merge pull request #10182 from peimanja/add-github-issues-label-action
feat(scaffolder-plugin-backend) Add GitHub issues label action
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added new `github:issues:label` action to apply labels to issues, and also output `pullRequestNumber` from `publish:github:pull-request`.
|
||||
@@ -137,6 +137,17 @@ export function createGithubActionsDispatchAction(options: {
|
||||
token?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function createGithubIssuesLabelAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
number: number;
|
||||
labels: string[];
|
||||
token?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export interface CreateGithubPullRequestActionOptions {
|
||||
clientFactory?: (
|
||||
@@ -368,6 +379,7 @@ export interface OctokitWithPullRequestPluginClient {
|
||||
createPullRequest(options: createPullRequest.Options): Promise<{
|
||||
data: {
|
||||
html_url: string;
|
||||
number: number;
|
||||
};
|
||||
} | null>;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import {
|
||||
createGithubActionsDispatchAction,
|
||||
createGithubWebhookAction,
|
||||
createGithubIssuesLabelAction,
|
||||
} from './github';
|
||||
import { TemplateFilter } from '../../../lib';
|
||||
import { TemplateAction } from '../types';
|
||||
@@ -145,6 +146,10 @@ export const createBuiltinActions = (
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
createGithubIssuesLabelAction({
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
];
|
||||
|
||||
return actions as TemplateAction<JsonObject>[];
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createGithubIssuesLabelAction } from './githubIssuesLabel';
|
||||
import {
|
||||
ScmIntegrations,
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { TemplateAction } from '../..';
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
issues: {
|
||||
addLabels: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.mock('octokit', () => ({
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('github:issues:label', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{ host: 'github.com', token: 'tokenlols' },
|
||||
{ host: 'ghe.github.com' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
let githubCredentialsProvider: GithubCredentialsProvider;
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
number: '1',
|
||||
labels: ['label1', 'label2'],
|
||||
},
|
||||
workspacePath: 'lol',
|
||||
logger: getVoidLogger(),
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
githubCredentialsProvider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
action = createGithubIssuesLabelAction({
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the githubApi for adding labels', async () => {
|
||||
await action.handler(mockContext);
|
||||
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
issue_number: '1',
|
||||
labels: ['label1', 'label2'],
|
||||
});
|
||||
});
|
||||
});
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { assertError, InputError } from '@backstage/errors';
|
||||
import { Octokit } from 'octokit';
|
||||
import { getOctokitOptions } from './helpers';
|
||||
import { parseRepoUrl } from '../publish/util';
|
||||
|
||||
/**
|
||||
* Adds labels to a pull request or issue on GitHub
|
||||
* @public
|
||||
*/
|
||||
export function createGithubIssuesLabelAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
const { integrations, githubCredentialsProvider } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
repoUrl: string;
|
||||
number: number;
|
||||
labels: string[];
|
||||
token?: string;
|
||||
}>({
|
||||
id: 'github:issues:label',
|
||||
description: 'Adds labels to a pull request or issue on GitHub.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['repoUrl', 'number', 'labels'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the repository name and 'owner' is an organization or username`,
|
||||
type: 'string',
|
||||
},
|
||||
number: {
|
||||
title: 'Pull Request or issue number',
|
||||
description: 'The pull request or issue number to add labels to',
|
||||
type: 'number',
|
||||
},
|
||||
labels: {
|
||||
title: 'Labels',
|
||||
description: 'The labels to add to the pull request or issue',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
token: {
|
||||
title: 'Authentication Token',
|
||||
type: 'string',
|
||||
description: 'The GITHUB_TOKEN to use for authorization to GitHub',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { repoUrl, number, labels, token: providedToken } = ctx.input;
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError('Invalid repository owner provided in repoUrl');
|
||||
}
|
||||
|
||||
const client = new Octokit(
|
||||
await getOctokitOptions({
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
repoUrl: repoUrl,
|
||||
token: providedToken,
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await client.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
labels,
|
||||
});
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
ctx.logger.warn(
|
||||
`Failed: adding labels to issue: '${number}' on repo: '${repo}', ${e.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { createGithubActionsDispatchAction } from './githubActionsDispatch';
|
||||
export { createGithubWebhookAction } from './githubWebhook';
|
||||
export { createGithubIssuesLabelAction } from './githubIssuesLabel';
|
||||
|
||||
+9
-4
@@ -58,6 +58,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
status: 201,
|
||||
data: {
|
||||
html_url: 'https://github.com/myorg/myrepo/pull/123',
|
||||
number: 123,
|
||||
},
|
||||
};
|
||||
}),
|
||||
@@ -123,13 +124,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -254,13 +256,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -322,13 +325,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -390,13 +394,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
|
||||
+10
-1
@@ -37,7 +37,10 @@ class GithubResponseError extends CustomErrorBase {}
|
||||
/** @public */
|
||||
export interface OctokitWithPullRequestPluginClient {
|
||||
createPullRequest(options: createPullRequest.Options): Promise<{
|
||||
data: { html_url: string };
|
||||
data: {
|
||||
html_url: string;
|
||||
number: number;
|
||||
};
|
||||
} | null>;
|
||||
}
|
||||
|
||||
@@ -169,6 +172,11 @@ export const createPublishGithubPullRequestAction = ({
|
||||
title: 'Pull Request URL',
|
||||
description: 'Link to the pull request in Github',
|
||||
},
|
||||
pullRequestNumber: {
|
||||
type: 'number',
|
||||
title: 'Pull Request Number',
|
||||
description: 'The pull request number',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,6 +271,7 @@ export const createPublishGithubPullRequestAction = ({
|
||||
}
|
||||
|
||||
ctx.output('remoteUrl', response.data.html_url);
|
||||
ctx.output('pullRequestNumber', response.data.number);
|
||||
} catch (e) {
|
||||
throw new GithubResponseError('Pull request creation failed', e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user