Skip to content

feat: ✨ Support PR Template on pull request creation #997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,8 @@ branches:

### Copy attributes from issue

When the App opens a new (draft) Pull Request it can also copy over the following attributes from your issue:
When the App opens a new (draft) Pull Request, it can also copy over the
following attributes from your issue:

- Description
- Labels
Expand All @@ -582,6 +583,18 @@ copyIssueProjectsToPR: true
copyIssueMilestoneToPR: true
```

### Copy pull request template

When the App opens a new (draft) Pull Request, it can also copy over a pull
request template from a file in your repository. The template must be stored in
a file named `.github/pull_request_template.md`.

You can enable this behaviour in the configuration:

```yaml
copyPullRequestTemplateToPR: true
```

### Skip CI workflows

Automatically opening a (draft) PR for an issue requires an empty commit on the newly created branch (this is a
Expand Down
2 changes: 2 additions & 0 deletions src/entities/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Config {
copyIssueProjectsToPR: boolean;
copyIssueMilestoneToPR: boolean;
copyIssueDescriptionToPR: boolean;
copyPullRequestTemplateToPR: boolean;
gitReplaceChars: string;
gitSafeReplacementChar: string;
commentMessage?: string;
Expand Down Expand Up @@ -49,6 +50,7 @@ export function getDefaultConfig(): Config {
copyIssueProjectsToPR: false,
copyIssueMilestoneToPR: false,
copyIssueDescriptionToPR: false,
copyPullRequestTemplateToPR: false,
gitReplaceChars: '',
gitSafeReplacementChar: '_',
silent: false,
Expand Down
44 changes: 34 additions & 10 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export async function createPr(app: Probot, ctx: Context<any>, config: Config, u
await createEmptyCommit(ctx, branchName, getCommitText(ctx, config), String(branchHeadSha))
}
const {data: pr} = await ctx.octokit.pulls.create(
{owner, repo, head: branchName, base, title, body: getPrBody(app, ctx, config), draft: draft})
{owner, repo, head: branchName, base, title, body: await getPrBody(app, ctx, config), draft: draft})
app.log.info(`${draft ? 'Created draft' : 'Created'} pull request ${pr.number} for branch ${branchName}`)
await copyIssueAttributesToPr(app, ctx, config, pr)
} catch (e: any) {
Expand Down Expand Up @@ -320,19 +320,43 @@ function getCommitText(ctx: Context<any>, config: Config) {
}
}

function getPrBody(app: Probot, ctx: Context<any>, config: Config) {
const issueNumber = getIssueNumber(ctx)
let result = ''
async function getPrBody(app: Probot, ctx: Context<any>, config: Config) {
const issueNumber = getIssueNumber(ctx);
let result = '';
if (config.copyIssueDescriptionToPR) {
app.log.info('Copying issue description to PR')
const issueDescription = getIssueDescription(ctx)
app.log.info('Copying issue description to PR');
const issueDescription = getIssueDescription(ctx);
if (issueDescription) {
result += formatAsExpandingMarkdown('Original issue description', issueDescription)
result += '\n'
result += formatAsExpandingMarkdown('Original issue description', issueDescription);
result += '\n';
}
}
result += `closes #${issueNumber}`
return result
if (config.copyPullRequestTemplateToPR) {
app.log.info('Copying pull-request template to PR');
const pullRequestTemplate = await getPullRequestTemplate(ctx);
if (pullRequestTemplate) {
result += pullRequestTemplate;
result += '\n';
}
}
result += `closes #${issueNumber}`;
return result;
}

async function getPullRequestTemplate(ctx: Context<any>): Promise<string | undefined> {
try {
const {data} = await ctx.octokit.repos.getContent({
owner: getRepoOwnerLogin(ctx),
repo: getRepoName(ctx),
path: '.github/pull_request_template.md'
}) as any;
if (data.type === 'file' && data.content) {
return Buffer.from(data.content, 'base64').toString('utf8');
}
} catch (e: any) {
/* do nothing */
}
return undefined;
}

async function copyIssueAttributesToPr(app: Probot, ctx: Context<any>, config: Config, pr: any) {
Expand Down
51 changes: 51 additions & 0 deletions tests/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ test('copy Issue description into PR', async () => {
})
})


test('Do not copy undefined Issue description into PR', async () => {
const createPR = jest.fn()
const ctx = getDefaultContext()
Expand All @@ -317,6 +318,56 @@ test('Do not copy undefined Issue description into PR', async () => {
})
})

test('copy pull-request template into PR', async () => {
const createPR = jest.fn();
const ctx = getDefaultContext();
ctx.octokit.pulls.create = createPR;
ctx.octokit.graphql = jest.fn();
ctx.octokit.repos.getContent = async (args: { owner: string, repo: string, path: string }) => {
expect(args.path).toBe('.github/pull_request_template.md');
return {data: {type: 'file', content: Buffer.from('file content').toString('base64')}};
};
const config = getDefaultConfig();
config.copyPullRequestTemplateToPR = true;

await github.createPr(probot, ctx, config, 'robvanderleek', 'issue-1');

expect(createPR).toHaveBeenCalledWith({
owner: 'robvanderleek',
repo: 'create-issue-branch',
head: 'issue-1',
base: 'master',
title: 'Hello world',
body: 'file content' + '\ncloses #1',
draft: false
});
})

test('pull-request template does not exist', async () => {
const createPR = jest.fn();
const ctx = getDefaultContext();
ctx.octokit.pulls.create = createPR;
ctx.octokit.graphql = jest.fn();
ctx.octokit.repos.getContent = async (args: { owner: string, repo: string, path: string }) => {
expect(args.path).toBe('.github/pull_request_template.md');
throw {status: 404};
};
const config = getDefaultConfig();
config.copyPullRequestTemplateToPR = true;

await github.createPr(probot, ctx, config, 'robvanderleek', 'issue-1');

expect(createPR).toHaveBeenCalledWith({
owner: 'robvanderleek',
repo: 'create-issue-branch',
head: 'issue-1',
base: 'master',
title: 'Hello world',
body: 'closes #1',
draft: false
});
})

test('use correct source branch', async () => {
const createPR = jest.fn()
const ctx = getDefaultContext()
Expand Down
1 change: 1 addition & 0 deletions tests/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export function getDefaultContext(): any {
createComment: () => {
}
},
repos: {},
graphql: (_: any, {message}: { message: string }) => {
}
}, //
Expand Down