feat: use mostly markdown for pr bodies (#1118)

Existing solution uses HTML bodies for PR descriptions, as that was the easiest way to get consistency between GitHub and GitLab. However, VSTS supports only markdown so we needed to refactor how this is done. Now, GitHub PR bodies uses only minimal HTML (for summary/details) while GitLab PR bodies are converted to HTML using GitHub flavoured markdown for maximum compatibility. VSTS will be able to strip out the minimal markdown.

Closes #1018
This commit is contained in:
Rhys Arkins 2017-11-08 11:09:26 +01:00 committed by GitHub
parent 89e13d05fe
commit 00e7821fcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 110 additions and 65 deletions

View file

@ -347,15 +347,17 @@ async function findPr(branchName, prTitle, state = 'all') {
// Pull Request // Pull Request
async function createPr(branchName, title, body, labels, useDefaultBranch) { async function createPr(
branchName,
title,
description,
labels,
useDefaultBranch
) {
const targetBranch = useDefaultBranch const targetBranch = useDefaultBranch
? config.defaultBranch ? config.defaultBranch
: config.baseBranch; : config.baseBranch;
logger.debug(`Creating Merge Request: ${title}`); logger.debug(`Creating Merge Request: ${title}`);
const description = body
.replace('</h4>', ' </h4>') // See #954
.replace(/Pull Request/g, 'Merge Request')
.replace(/PR/g, 'MR');
const res = await get.post(`projects/${config.repoName}/merge_requests`, { const res = await get.post(`projects/${config.repoName}/merge_requests`, {
body: { body: {
source_branch: branchName, source_branch: branchName,
@ -400,11 +402,7 @@ async function getPr(prNo) {
return pr; return pr;
} }
async function updatePr(prNo, title, body) { async function updatePr(prNo, title, description) {
const description = body
.replace('</h4>', ' </h4>') // See #954
.replace(/Pull Request/g, 'Merge Request')
.replace(/PR/g, 'MR');
await get.put(`projects/${config.repoName}/merge_requests/${prNo}`, { await get.put(`projects/${config.repoName}/merge_requests/${prNo}`, {
body: { body: {
title, title,

View file

@ -129,22 +129,23 @@ async function ensurePr(prConfig) {
} }
const prTitle = handlebars.compile(config.prTitle)(config); const prTitle = handlebars.compile(config.prTitle)(config);
let prBody; let prBody = handlebars.compile(config.prBody)(config);
function trimPrBody() { // Put a zero width space after every @ symbol to prevent unintended hyperlinking
let prBodyMarkdown = handlebars.compile(config.prBody)(config); prBody = prBody.replace('@', '@&#8203;');
prBodyMarkdown = prBodyMarkdown.replace('@', '@&#8203;'); // Public GitHub repos need links prevented - see #489
prBody = converter.makeHtml(prBodyMarkdown); prBody = prBody.replace(issueRe, '$1#&#8203;$2$3');
// Public GitHub repos need links prevented - see #489 // convert escaped backticks back to `
prBody = prBody.replace(issueRe, '$1#&#8203;$2$3'); const backTickRe = /&#x60;([^/]*?)&#x60;/g;
const backTickRe = /&#x60;([^/]*?)&#x60;/g; prBody = prBody.replace(backTickRe, '`$1`');
prBody = prBody.replace(backTickRe, '<code>$1</code>'); // It would be nice to abstract this to the platform layer but made difficult due to our create/update check
// istanbul ignore if if (config.platform === 'gitlab') {
if (prBody.length > 250000) { // Convert to HTML using GitHub-flavoured markdown as it is more feature-rich than GitLab's flavour
config.upgrades.pop(); prBody = converter
trimPrBody(); .makeHtml(prBody)
} .replace('</h4>', ' </h4>') // See #954
.replace(/Pull Request/g, 'Merge Request')
.replace(/PR/g, 'MR');
} }
trimPrBody();
try { try {
// Check if existing PR exists // Check if existing PR exists

View file

@ -26,19 +26,71 @@ Array [
] ]
`; `;
exports[`workers/pr ensurePr should return modified existing PR 1`] = ` exports[`workers/pr ensurePr should convert to HTML PR for gitlab 1`] = `
Object { Array [
"body": "<p>This Pull Request updates dependency <a href=\\"https://github.com/renovateapp/dummy\\">dummy</a> from <code>v1.0.0</code> to <code>v1.1.0</code></p> "renovate/dummy-1.x",
"Update dependency dummy to v1.1.0",
"<p>This Merge Request updates dependency <a href=\\"https://github.com/renovateapp/dummy\\">dummy</a> from <code>v1.0.0</code> to <code>v1.1.0</code></p>
<h3 id=\\"commits\\">Commits</h3> <h3 id=\\"commits\\">Commits</h3>
<p><details><br /> <p><details><br />
<summary>renovateapp/dummy</summary></p> <summary>renovateapp/dummy</summary></p>
<h4 id=\\"110\\">1.1.0</h4> <h4 id=\\"110\\">1.1.0 </h4>
<ul> <ul>
<li><a href=\\"https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz\\"><code>abcdefg</code></a> foo <a href=\\"https://github.com/renovateapp/dummy/issues/3\\">#3</a></li> <li><a href=\\"https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz\\"><code>abcdefg</code></a> foo <a href=\\"https://github.com/renovateapp/dummy/issues/3\\">#3</a></li>
</ul> </ul>
<p></details></p> <p></details></p>
<hr /> <hr />
<p>This PR has been generated by <a href=\\"https://renovateapp.com\\">Renovate Bot</a>.</p>", <p>This MR has been generated by <a href=\\"https://renovateapp.com\\">Renovate Bot</a>.</p>",
Array [],
]
`;
exports[`workers/pr ensurePr should create PR if success 1`] = `
Array [
"renovate/dummy-1.x",
"Update dependency dummy to v1.1.0",
"This Pull Request updates dependency [dummy](https://github.com/renovateapp/dummy) from \`v1.0.0\` to \`v1.1.0\`
### Commits
<details>
<summary>renovateapp/dummy</summary>
#### 1.1.0
- [\`abcdefg\`](https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz) foo [#3](https://github.com/renovateapp/dummy/issues/3)
</details>
---
This PR has been generated by [Renovate Bot](https://renovateapp.com).",
Array [],
]
`;
exports[`workers/pr ensurePr should return modified existing PR 1`] = `
Object {
"body": "This Pull Request updates dependency [dummy](https://github.com/renovateapp/dummy) from \`v1.0.0\` to \`v1.1.0\`
### Commits
<details>
<summary>renovateapp/dummy</summary>
#### 1.1.0
- [\`abcdefg\`](https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz) foo [#3](https://github.com/renovateapp/dummy/issues/3)
</details>
---
This PR has been generated by [Renovate Bot](https://renovateapp.com).",
"displayNumber": "Existing PR", "displayNumber": "Existing PR",
"title": "Update dependency dummy to v1.1.0", "title": "Update dependency dummy to v1.1.0",
} }

View file

@ -86,28 +86,24 @@ describe('workers/pr', () => {
}); });
describe('ensurePr', () => { describe('ensurePr', () => {
let config; let config;
let existingPr; const existingPr = {
displayNumber: 'Existing PR',
title: 'Update dependency dummy to v1.1.0',
};
beforeEach(() => { beforeEach(() => {
config = { config = {
...defaultConfig, ...defaultConfig,
}; };
config.branchName = 'renovate/dummy-1.x';
config.prTitle = 'Update dependency dummy to v1.1.0';
config.depName = 'dummy';
config.isGitHub = true;
config.privateRepo = true;
config.currentVersion = '1.0.0';
config.newVersion = '1.1.0';
config.repositoryUrl = 'https://github.com/renovateapp/dummy';
platform.createPr.mockReturnValue({ displayNumber: 'New Pull Request' }); platform.createPr.mockReturnValue({ displayNumber: 'New Pull Request' });
config.upgrades = [config]; config.upgrades = [config];
existingPr = {
title: 'Update dependency dummy to v1.1.0',
body: `<p>This Pull Request updates dependency <a href="https://github.com/renovateapp/dummy">dummy</a> from <code>v1.0.0</code> to <code>v1.1.0</code></p>
<h3 id="commits">Commits</h3>
<p><details><br />
<summary>renovateapp/dummy</summary></p>
<h4 id="110">1.1.0</h4>
<ul>
<li><a href="https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz"><code>abcdefg</code></a> foo <a href="https://github.com/renovateapp/dummy/issues/3">#3</a></li>
</ul>
<p></details></p>
<hr />
<p>This PR has been generated by <a href="https://renovateapp.com">Renovate Bot</a>.</p>`,
displayNumber: 'Existing PR',
};
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -130,6 +126,19 @@ describe('workers/pr', () => {
config.prCreation = 'status-success'; config.prCreation = 'status-success';
const pr = await prWorker.ensurePr(config); const pr = await prWorker.ensurePr(config);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
expect(platform.createPr.mock.calls[0]).toMatchSnapshot();
existingPr.body = platform.createPr.mock.calls[0][2];
});
it('should convert to HTML PR for gitlab', async () => {
platform.getBranchStatus.mockReturnValueOnce('success');
config.prCreation = 'status-success';
config.platform = 'gitlab';
const pr = await prWorker.ensurePr(config);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
expect(platform.createPr.mock.calls[0]).toMatchSnapshot();
expect(platform.createPr.mock.calls[0][2].indexOf('<p>This MR')).not.toBe(
-1
);
}); });
it('should delete branch and return null if creating PR fails', async () => { it('should delete branch and return null if creating PR fails', async () => {
platform.getBranchStatus.mockReturnValueOnce('success'); platform.getBranchStatus.mockReturnValueOnce('success');
@ -210,10 +219,10 @@ describe('workers/pr', () => {
config.warnings = [{}]; config.warnings = [{}];
const pr = await prWorker.ensurePr(config); const pr = await prWorker.ensurePr(config);
expect( expect(
platform.createPr.mock.calls[0][2].indexOf('Errors</h3>') platform.createPr.mock.calls[0][2].indexOf('### Errors')
).not.toEqual(-1); ).not.toEqual(-1);
expect( expect(
platform.createPr.mock.calls[0][2].indexOf('Warnings</h3>') platform.createPr.mock.calls[0][2].indexOf('### Warnings')
).not.toEqual(-1); ).not.toEqual(-1);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
}); });
@ -227,15 +236,9 @@ describe('workers/pr', () => {
expect(platform.addReviewers.mock.calls.length).toBe(0); expect(platform.addReviewers.mock.calls.length).toBe(0);
}); });
it('should add assignees and reviewers to existing PR', async () => { it('should add assignees and reviewers to existing PR', async () => {
config.depName = 'dummy';
config.automerge = true;
config.assignees = ['bar']; config.assignees = ['bar'];
config.reviewers = ['baz']; config.reviewers = ['baz'];
config.isGitHub = true; config.automerge = true;
config.privateRepo = true;
config.currentVersion = '1.0.0';
config.newVersion = '1.1.0';
config.repositoryUrl = 'https://github.com/renovateapp/dummy';
platform.getBranchPr.mockReturnValueOnce(existingPr); platform.getBranchPr.mockReturnValueOnce(existingPr);
platform.getBranchStatus.mockReturnValueOnce('failure'); platform.getBranchStatus.mockReturnValueOnce('failure');
config.semanticPrefix = ''; config.semanticPrefix = '';
@ -247,24 +250,15 @@ describe('workers/pr', () => {
expect(pr).toMatchObject(existingPr); expect(pr).toMatchObject(existingPr);
}); });
it('should return unmodified existing PR', async () => { it('should return unmodified existing PR', async () => {
config.depName = 'dummy';
config.isGitHub = true;
config.privateRepo = true;
config.currentVersion = '1.0.0';
config.newVersion = '1.1.0';
config.repositoryUrl = 'https://github.com/renovateapp/dummy';
platform.getBranchPr.mockReturnValueOnce(existingPr); platform.getBranchPr.mockReturnValueOnce(existingPr);
config.semanticPrefix = ''; config.semanticPrefix = '';
const pr = await prWorker.ensurePr(config); const pr = await prWorker.ensurePr(config);
expect(platform.updatePr.mock.calls).toMatchSnapshot(); expect(platform.updatePr.mock.calls).toMatchSnapshot();
expect(platform.updatePr.mock.calls.length).toBe(0); expect(platform.updatePr.mock.calls).toHaveLength(0);
expect(pr).toMatchObject(existingPr); expect(pr).toMatchObject(existingPr);
}); });
it('should return modified existing PR', async () => { it('should return modified existing PR', async () => {
config.depName = 'dummy';
config.currentVersion = '1.0.0';
config.newVersion = '1.2.0'; config.newVersion = '1.2.0';
config.isGitHub = true;
platform.getBranchPr.mockReturnValueOnce(existingPr); platform.getBranchPr.mockReturnValueOnce(existingPr);
const pr = await prWorker.ensurePr(config); const pr = await prWorker.ensurePr(config);
expect(pr).toMatchSnapshot(); expect(pr).toMatchSnapshot();