diff --git a/README.md b/README.md index 24d7f213..affc2022 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/entities/Config.ts b/src/entities/Config.ts index d13cd3cf..0d03864a 100644 --- a/src/entities/Config.ts +++ b/src/entities/Config.ts @@ -11,6 +11,7 @@ export interface Config { copyIssueProjectsToPR: boolean; copyIssueMilestoneToPR: boolean; copyIssueDescriptionToPR: boolean; + copyPullRequestTemplateToPR: boolean; gitReplaceChars: string; gitSafeReplacementChar: string; commentMessage?: string; @@ -49,6 +50,7 @@ export function getDefaultConfig(): Config { copyIssueProjectsToPR: false, copyIssueMilestoneToPR: false, copyIssueDescriptionToPR: false, + copyPullRequestTemplateToPR: false, gitReplaceChars: '', gitSafeReplacementChar: '_', silent: false, diff --git a/src/github.ts b/src/github.ts index 0f335aac..bb71afcd 100644 --- a/src/github.ts +++ b/src/github.ts @@ -276,7 +276,7 @@ export async function createPr(app: Probot, ctx: Context, 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) { @@ -320,19 +320,43 @@ function getCommitText(ctx: Context, config: Config) { } } -function getPrBody(app: Probot, ctx: Context, config: Config) { - const issueNumber = getIssueNumber(ctx) - let result = '' +async function getPrBody(app: Probot, ctx: Context, 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): Promise { + 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, config: Config, pr: any) { diff --git a/tests/github.test.ts b/tests/github.test.ts index cb43ed54..e8e11cf3 100644 --- a/tests/github.test.ts +++ b/tests/github.test.ts @@ -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() @@ -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() diff --git a/tests/test-helpers.ts b/tests/test-helpers.ts index 6611fa98..d2d5ce8f 100644 --- a/tests/test-helpers.ts +++ b/tests/test-helpers.ts @@ -163,6 +163,7 @@ export function getDefaultContext(): any { createComment: () => { } }, + repos: {}, graphql: (_: any, {message}: { message: string }) => { } }, //