Merge pull request #24628 from mahenrique94/author-name-email-commit-pr

Adding support to override default author commit for github PRs
This commit is contained in:
Ben Lambert
2024-05-08 19:41:07 +02:00
committed by GitHub
9 changed files with 621 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Adding support to change the default commit author for `publish:github:pull-request`
@@ -128,6 +128,7 @@ export interface CreateGithubPullRequestActionOptions {
} | null>;
}
>;
config?: Config;
githubCredentialsProvider?: GithubCredentialsProvider;
integrations: ScmIntegrationRegistry;
}
@@ -389,6 +390,8 @@ export const createPublishGithubPullRequestAction: (
commitMessage?: string | undefined;
update?: boolean | undefined;
forceFork?: boolean | undefined;
gitAuthorName?: string | undefined;
gitAuthorEmail?: string | undefined;
},
JsonObject
>;
@@ -103,6 +103,7 @@ describe('publish:github:pull-request examples', () => {
integrations,
githubCredentialsProvider,
clientFactory,
config,
});
});
@@ -484,6 +485,138 @@ describe('publish:github:pull-request examples', () => {
expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123);
});
it('Create a pull request with a git author name and email', async () => {
const input = yaml.parse(examples[9].example).steps[0].input;
await action.handler({
...mockContext,
workspacePath,
input,
});
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
title: 'Create my new app',
body: 'This PR is really good',
head: 'new-app',
draft: undefined,
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Foo Bar',
},
},
],
});
expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled();
expect(mockContext.output).toHaveBeenCalledTimes(3);
expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main');
expect(mockContext.output).toHaveBeenCalledWith(
'remoteUrl',
'https://github.com/myorg/myrepo/pull/123',
);
expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123);
});
it('Create a pull request with a git author name', async () => {
const input = yaml.parse(examples[10].example).steps[0].input;
await action.handler({
...mockContext,
workspacePath,
input,
});
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
title: 'Create my new app',
body: 'This PR is really good',
head: 'new-app',
draft: undefined,
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'scaffolder@backstage.io',
name: 'Foo Bar',
},
},
],
});
expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled();
expect(mockContext.output).toHaveBeenCalledTimes(3);
expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main');
expect(mockContext.output).toHaveBeenCalledWith(
'remoteUrl',
'https://github.com/myorg/myrepo/pull/123',
);
expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123);
});
it('Create a pull request with a git author email', async () => {
const input = yaml.parse(examples[11].example).steps[0].input;
await action.handler({
...mockContext,
workspacePath,
input,
});
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
title: 'Create my new app',
body: 'This PR is really good',
head: 'new-app',
draft: undefined,
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Scaffolder',
},
},
],
});
expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled();
expect(mockContext.output).toHaveBeenCalledTimes(3);
expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main');
expect(mockContext.output).toHaveBeenCalledWith(
'remoteUrl',
'https://github.com/myorg/myrepo/pull/123',
);
expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123);
});
it('Create a pull request with all parameters', async () => {
mockDir.setContent({
[workspacePath]: {
@@ -491,7 +624,7 @@ describe('publish:github:pull-request examples', () => {
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const input = yaml.parse(examples[9].example).steps[0].input;
const input = yaml.parse(examples[12].example).steps[0].input;
await action.handler({
...mockContext,
@@ -517,6 +650,10 @@ describe('publish:github:pull-request examples', () => {
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Foo Bar',
},
},
],
});
@@ -178,6 +178,65 @@ export const examples: TemplateExample[] = [
],
}),
},
{
description: 'Create a pull request with a git author name and email',
example: yaml.stringify({
steps: [
{
action: 'publish:github:pull-request',
name: 'Create a pull reuqest',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
branchName: 'new-app',
title: 'Create my new app',
description: 'This PR is really good',
gitAuthorName: 'Foo Bar',
gitAuthorEmail: 'foo@bar.example',
},
},
],
}),
},
{
description: 'Create a pull request with a git author name',
example: yaml.stringify({
steps: [
{
action: 'publish:github:pull-request',
name: 'Create a pull reuqest',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
branchName: 'new-app',
title: 'Create my new app',
description: 'This PR is really good',
// gitAuthorEmail will be 'scaffolder@backstage.io'
// once one author attribute has been set we need to set both
gitAuthorName: 'Foo Bar',
},
},
],
}),
},
{
description: 'Create a pull request with a git author email',
example: yaml.stringify({
steps: [
{
action: 'publish:github:pull-request',
name: 'Create a pull reuqest',
input: {
repoUrl: 'github.com?repo=repo&owner=owner',
branchName: 'new-app',
title: 'Create my new app',
description: 'This PR is really good',
// gitAuthorName will be 'Scaffolder'
// once one author attribute has been set we need to set both
gitAuthorEmail: 'foo@bar.example',
},
},
],
}),
},
{
description: 'Create a pull request with all parameters',
example: yaml.stringify({
@@ -198,6 +257,8 @@ export const examples: TemplateExample[] = [
reviewers: ['foobar'],
teamReviewers: ['team-foo'],
commitMessage: 'Commit for foo changes',
gitAuthorName: 'Foo Bar',
gitAuthorEmail: 'foo@bar.example',
},
},
],
@@ -15,7 +15,7 @@
*/
import { createRootLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Config, ConfigReader } from '@backstage/config';
import {
GithubCredentialsProvider,
ScmIntegrations,
@@ -46,6 +46,8 @@ describe('createPublishGithubPullRequestAction', () => {
pulls: { requestReviewers: jest.Mock };
};
};
let config: Config;
let integrations: ScmIntegrations;
const mockDir = createMockDirectory();
const workspacePath = mockDir.resolve('workspace');
@@ -53,7 +55,8 @@ describe('createPublishGithubPullRequestAction', () => {
beforeEach(() => {
mockDir.clear();
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
config = new ConfigReader({});
integrations = ScmIntegrations.fromConfig(config);
fakeClient = {
createPullRequest: jest.fn(async (_: any) => {
return {
@@ -84,6 +87,7 @@ describe('createPublishGithubPullRequestAction', () => {
integrations,
githubCredentialsProvider,
clientFactory,
config,
});
});
@@ -663,4 +667,352 @@ describe('createPublishGithubPullRequestAction', () => {
});
});
});
describe('with author name and email', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
gitAuthorEmail: 'foo@bar.example',
gitAuthorName: 'Foo Bar',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request', async () => {
await instance.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Foo Bar',
},
},
],
});
});
});
describe('with author name', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
gitAuthorName: 'Foo Bar',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request', async () => {
await instance.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'scaffolder@backstage.io',
name: 'Foo Bar',
},
},
],
});
});
});
describe('with author email', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
gitAuthorEmail: 'foo@bar.example',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request', async () => {
await instance.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Scaffolder',
},
},
],
});
});
});
describe('with author from config file', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request with default config attributes', async () => {
config = new ConfigReader({
scaffolder: {
defaultAuthor: {
name: 'Config',
email: 'config@file.example',
},
},
});
const clientFactory = jest.fn(async () => fakeClient as any);
const githubCredentialsProvider: GithubCredentialsProvider = {
getCredentials: jest.fn(),
};
const instanceWithConfig = createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
clientFactory,
config,
});
await instanceWithConfig.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'config@file.example',
name: 'Config',
},
},
],
});
});
});
describe('with author attributes and config file', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
gitAuthorEmail: 'foo@bar.example',
gitAuthorName: 'Foo Bar',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request with using author name and email from input', async () => {
config = new ConfigReader({
scaffolder: {
defaultAuthor: {
name: 'Config',
email: 'config@file.example',
},
},
});
const clientFactory = jest.fn(async () => fakeClient as any);
const githubCredentialsProvider: GithubCredentialsProvider = {
getCredentials: jest.fn(),
};
const instanceWithConfig = createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
clientFactory,
config,
});
await instanceWithConfig.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'foo@bar.example',
name: 'Foo Bar',
},
},
],
});
});
});
describe('with author fallback and no config', () => {
let input: GithubPullRequestActionInput;
let ctx: ActionContext<GithubPullRequestActionInput>;
beforeEach(() => {
input = {
repoUrl: 'github.com?owner=myorg&repo=myrepo',
title: 'Create my new app',
branchName: 'new-app',
description: 'This PR is really good',
gitAuthorName: 'Foo Bar',
};
mockDir.setContent({
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = createMockActionContext({ input, workspacePath });
});
it('creates a pull request with using author name and email fallback when have no config', async () => {
const clientFactory = jest.fn(async () => fakeClient as any);
const githubCredentialsProvider: GithubCredentialsProvider = {
getCredentials: jest.fn(),
};
const instanceWithConfig = createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
clientFactory,
});
await instanceWithConfig.handler(ctx);
expect(fakeClient.createPullRequest).toHaveBeenCalledWith({
owner: 'myorg',
repo: 'myrepo',
title: 'Create my new app',
head: 'new-app',
body: 'This PR is really good',
changes: [
{
commit: 'Create my new app',
files: {
'file.txt': {
content: Buffer.from('Hello there!').toString('base64'),
encoding: 'base64',
mode: '100644',
},
},
author: {
email: 'scaffolder@backstage.io',
name: 'Foo Bar',
},
},
],
});
});
});
});
@@ -32,6 +32,7 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request';
import { getOctokitOptions } from './helpers';
import { examples } from './githubPullRequest.examples';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
export type Encoding = 'utf-8' | 'base64';
@@ -100,6 +101,10 @@ export interface CreateGithubPullRequestActionOptions {
} | null>;
}
>;
/**
* An instance of {@link @backstage/config#Config} that will be used in the action.
*/
config?: Config;
}
type GithubPullRequest = {
@@ -119,6 +124,7 @@ export const createPublishGithubPullRequestAction = (
integrations,
githubCredentialsProvider,
clientFactory = defaultClientFactory,
config,
} = options;
return createTemplateAction<{
@@ -136,6 +142,8 @@ export const createPublishGithubPullRequestAction = (
commitMessage?: string;
update?: boolean;
forceFork?: boolean;
gitAuthorName?: string;
gitAuthorEmail?: string;
}>({
id: 'publish:github:pull-request',
examples,
@@ -223,6 +231,18 @@ export const createPublishGithubPullRequestAction = (
title: 'Force Fork',
description: 'Create pull request from a fork',
},
gitAuthorName: {
type: 'string',
title: 'Default Author Name',
description:
"Sets the default author name for the commit. The default value is the authenticated user or 'Scaffolder'",
},
gitAuthorEmail: {
type: 'string',
title: 'Default Author Email',
description:
"Sets the default author email for the commit. The default value is the authenticated user or 'scaffolder@backstage.io'",
},
},
},
output: {
@@ -262,6 +282,8 @@ export const createPublishGithubPullRequestAction = (
commitMessage,
update,
forceFork,
gitAuthorEmail,
gitAuthorName,
} = ctx.input;
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
@@ -327,7 +349,10 @@ export const createPublishGithubPullRequestAction = (
changes: [
{
files,
commit: commitMessage ?? title,
commit:
commitMessage ??
config?.getOptionalString('scaffolder.defaultCommitMessage') ??
title,
},
],
body: description,
@@ -336,6 +361,36 @@ export const createPublishGithubPullRequestAction = (
update,
forceFork,
};
const gitAuthorInfo = {
name:
gitAuthorName ??
config?.getOptionalString('scaffolder.defaultAuthor.name'),
email:
gitAuthorEmail ??
config?.getOptionalString('scaffolder.defaultAuthor.email'),
};
if (gitAuthorInfo.name || gitAuthorInfo.email) {
if (Array.isArray(createOptions.changes)) {
createOptions.changes = createOptions.changes.map(change => ({
...change,
author: {
name: gitAuthorInfo.name || 'Scaffolder',
email: gitAuthorInfo.email || 'scaffolder@backstage.io',
},
}));
} else {
createOptions.changes = {
...createOptions.changes,
author: {
name: gitAuthorInfo.name || 'Scaffolder',
email: gitAuthorInfo.email || 'scaffolder@backstage.io',
},
};
}
}
if (targetBranchName) {
createOptions.base = targetBranchName;
}
@@ -89,6 +89,7 @@ export const githubModule = createBackendModule({
createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
config,
}),
);
},
+2
View File
@@ -287,6 +287,8 @@ export const createPublishGithubPullRequestAction: (
commitMessage?: string | undefined;
update?: boolean | undefined;
forceFork?: boolean | undefined;
gitAuthorName?: string | undefined;
gitAuthorEmail?: string | undefined;
},
JsonObject
>;
@@ -178,6 +178,7 @@ export const createBuiltinActions = (
createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
config,
}),
createPublishGitlabAction({
integrations,