feat: add github:autolinks:create action

Signed-off-by: Rutuja Marathe <rutujasudam.marathe@factset.com>
This commit is contained in:
Rutuja Marathe
2023-12-13 16:05:41 -05:00
parent 973bc8cf90
commit 28949ea4aa
8 changed files with 398 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
'@backstage/plugin-scaffolder-backend': patch
---
Add a new action for creating github-autolink references for a repository: `github:autolinks:create`
@@ -31,6 +31,21 @@ export function createGithubActionsDispatchAction(options: {
JsonObject
>;
// @public
export function createGithubAutolinksAction(options: {
integrations: ScmIntegrations;
githubCredentialsProvider?: GithubCredentialsProvider;
}): TemplateAction<
{
repoUrl: string;
keyPrefix: string;
urlTemplate: string;
isAlphanumeric?: boolean | undefined;
token?: string | undefined;
},
JsonObject
>;
// @public
export function createGithubDeployKeyAction(options: {
integrations: ScmIntegrationRegistry;
@@ -0,0 +1,90 @@
/*
* Copyright 2023 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 { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { PassThrough } from 'stream';
import { createGithubAutolinksAction } from './githubAutolinks';
import { examples } from './githubAutolinks.examples';
import yaml from 'yaml';
const mockOctokit = {
rest: {
repos: {
createAutolink: jest.fn(),
},
},
};
jest.mock('octokit', () => ({
Octokit: class {
constructor() {
return mockOctokit;
}
},
}));
describe('github:autolinks:create', () => {
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, any>;
it('should call the githubApis for creating autolink reference', async () => {
const input = yaml.parse(examples[0].example).steps[0].input;
githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
action = createGithubAutolinksAction({
integrations,
githubCredentialsProvider,
});
mockOctokit.rest.repos.createAutolink.mockResolvedValue({
data: {
id: '1',
},
});
await action.handler({
input,
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
});
expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
key_prefix: 'TICKET-',
url_template: 'https://example.com/TICKET?query=<num>',
is_alphanumeric: false,
});
});
});
@@ -0,0 +1,38 @@
/*
* Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node';
import yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description: 'GitHub alphanumric autolink reference',
example: yaml.stringify({
steps: [
{
action: 'github:autolinks:create',
name: 'Create an autolink reference',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
keyPrefix: 'TICKET-',
urlTemplate: 'https://example.com/TICKET?query=<num>',
isAlphanumeric: false,
},
},
],
}),
},
];
@@ -0,0 +1,125 @@
/*
* Copyright 2023 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 { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { PassThrough } from 'stream';
import { createGithubAutolinksAction } from './githubAutolinks';
const mockOctokit = {
rest: {
repos: {
createAutolink: jest.fn(),
},
},
};
jest.mock('octokit', () => ({
Octokit: class {
constructor() {
return mockOctokit;
}
},
}));
describe('github:autolinks:create', () => {
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, any>;
const mockContext = {
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
it('should call the githubApis for creating alphanumeric autolink reference', async () => {
githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
action = createGithubAutolinksAction({
integrations,
githubCredentialsProvider,
});
mockOctokit.rest.repos.createAutolink.mockResolvedValue({
data: {
id: '1',
},
});
await action.handler({
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
keyPrefix: 'TICKET-',
urlTemplate: 'https://example.com/TICKET?query=<num>',
},
...mockContext,
});
expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
key_prefix: 'TICKET-',
url_template: 'https://example.com/TICKET?query=<num>',
});
});
it('should call the githubApis for creating numeric autolink reference', async () => {
githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
action = createGithubAutolinksAction({
integrations,
githubCredentialsProvider,
});
mockOctokit.rest.repos.createAutolink.mockResolvedValue({
data: {
id: '1',
},
});
await action.handler({
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
keyPrefix: 'TICKET-',
urlTemplate: 'https://example.com/TICKET?query=<num>',
isAlphanumeric: false,
},
...mockContext,
});
expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
key_prefix: 'TICKET-',
url_template: 'https://example.com/TICKET?query=<num>',
is_alphanumeric: false,
});
});
});
@@ -0,0 +1,118 @@
/*
* 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 { InputError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { Octokit } from 'octokit';
import { examples } from './githubAutolinks.examples';
import { getOctokitOptions } from './helpers';
/**
* Create an autolink reference for a repository
* @public
*/
export function createGithubAutolinksAction(options: {
integrations: ScmIntegrations;
githubCredentialsProvider?: GithubCredentialsProvider;
}) {
const { integrations, githubCredentialsProvider } = options;
return createTemplateAction<{
repoUrl: string;
keyPrefix: string;
urlTemplate: string;
isAlphanumeric?: boolean;
token?: string;
}>({
id: 'github:autolinks:create',
description: 'Create an autolink reference for a repository',
examples,
schema: {
input: {
type: 'object',
required: ['repoUrl', 'keyPrefix', 'urlTemplate'],
properties: {
repoUrl: {
title: 'Repository Location',
description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`,
type: 'string',
},
keyPrefix: {
title: 'Key Prefix',
description:
'This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit.',
type: 'string',
},
urlTemplate: {
title: 'URL Template',
description:
'The URL must contain <num> for the reference number. <num> matches different characters depending on the value of isAlphanumeric.',
type: 'string',
},
isAlphanumeric: {
title: 'Alphanumeric',
description:
'Whether this autolink reference matches alphanumeric characters. If true, the <num> parameter of the url_template matches alphanumeric characters A-Z (case insensitive), 0-9, and -. If false, this autolink reference only matches numeric characters. Default: true',
type: 'boolean',
},
token: {
title: 'Authentication Token',
type: 'string',
description: 'The token to use for authorization to GitHub',
},
},
},
},
async handler(ctx) {
const { repoUrl, keyPrefix, urlTemplate, isAlphanumeric, token } =
ctx.input;
ctx.logger.info(`Creating autolink reference for repo ${repoUrl}`);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const client = new Octokit(
await getOctokitOptions({
integrations,
repoUrl,
credentialsProvider: githubCredentialsProvider,
token,
}),
);
await client.rest.repos.createAutolink({
owner,
repo,
key_prefix: keyPrefix,
url_template: urlTemplate,
is_alphanumeric: isAlphanumeric,
});
ctx.logger.info(`Autolink reference created successfully`);
},
});
}
@@ -27,3 +27,4 @@ export {
type CreateGithubPullRequestActionOptions,
} from './githubPullRequest';
export { createPublishGithubAction } from './github';
export { createGithubAutolinksAction } from './githubAutolinks';
@@ -42,6 +42,7 @@ import {
} from './filesystem';
import {
createGithubActionsDispatchAction,
createGithubAutolinksAction,
createGithubDeployKeyAction,
createGithubEnvironmentAction,
createGithubIssuesLabelAction,
@@ -218,6 +219,10 @@ export const createBuiltinActions = (
createGithubDeployKeyAction({
integrations,
}),
createGithubAutolinksAction({
integrations,
githubCredentialsProvider,
}),
];
return actions as TemplateAction[];