feat(github): reuse autoclosed PRs (#9300)

This commit is contained in:
Rhys Arkins 2021-03-28 11:02:15 +02:00 committed by GitHub
parent ca0cf2e6cd
commit eb2873e22e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 593 additions and 66 deletions

View file

@ -1873,6 +1873,400 @@ Array [
] ]
`; `;
exports[`platform/github getBranchPr(branchName) aborts reopening if PR reopening fails 1`] = `
Array [
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"defaultBranchRef": Object {
"name": null,
"target": Object {
"oid": null,
},
},
"isArchived": null,
"isFork": null,
"mergeCommitAllowed": null,
"nameWithOwner": null,
"rebaseMergeAllowed": null,
"squashMergeAllowed": null,
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "330",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/graphql",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://api.github.com/repos/some/repo/pulls?per_page=100&state=all",
},
Object {
"body": "{\\"ref\\":\\"refs/heads/somebranch\\"}",
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "31",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/repos/some/repo/git/refs",
},
]
`;
exports[`platform/github getBranchPr(branchName) aborts reopening if branch recreation fails 1`] = `
Array [
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"defaultBranchRef": Object {
"name": null,
"target": Object {
"oid": null,
},
},
"isArchived": null,
"isFork": null,
"mergeCommitAllowed": null,
"nameWithOwner": null,
"rebaseMergeAllowed": null,
"squashMergeAllowed": null,
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "330",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/graphql",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://api.github.com/repos/some/repo/pulls?per_page=100&state=all",
},
Object {
"body": "{\\"ref\\":\\"refs/heads/somebranch\\"}",
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "31",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/repos/some/repo/git/refs",
},
Object {
"body": "{\\"state\\":\\"open\\",\\"title\\":\\"old title\\"}",
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "36",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "PATCH",
"url": "https://api.github.com/repos/some/repo/pulls/91",
},
]
`;
exports[`platform/github getBranchPr(branchName) should reopen an autoclosed PR 1`] = `
Object {
"additions": 1,
"base": Object {
"sha": "1234",
},
"canMerge": false,
"canMergeReason": "mergeable = undefined",
"commits": 1,
"deletions": 1,
"displayNumber": "Pull Request #91",
"head": Object {
"ref": "somebranch",
"repo": Object {
"full_name": "some/repo",
},
},
"number": 91,
"sha": undefined,
"sourceBranch": "somebranch",
"state": "open",
}
`;
exports[`platform/github getBranchPr(branchName) should reopen an autoclosed PR 2`] = `
Array [
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"defaultBranchRef": Object {
"name": null,
"target": Object {
"oid": null,
},
},
"isArchived": null,
"isFork": null,
"mergeCommitAllowed": null,
"nameWithOwner": null,
"rebaseMergeAllowed": null,
"squashMergeAllowed": null,
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "330",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/graphql",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://api.github.com/repos/some/repo/pulls?per_page=100&state=all",
},
Object {
"body": "{\\"ref\\":\\"refs/heads/somebranch\\"}",
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "31",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/repos/some/repo/git/refs",
},
Object {
"body": "{\\"state\\":\\"open\\",\\"title\\":\\"old title\\"}",
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "36",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "PATCH",
"url": "https://api.github.com/repos/some/repo/pulls/91",
},
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"pullRequests": Object {
"__args": Object {
"first": "100",
},
"nodes": Object {
"assignees": Object {
"totalCount": null,
},
"baseRefName": null,
"body": null,
"commits": Object {
"__args": Object {
"first": "2",
},
"nodes": Object {
"commit": Object {
"author": Object {
"email": null,
},
"committer": Object {
"email": null,
},
"parents": Object {
"__args": Object {
"last": "1",
},
"edges": Object {
"node": Object {
"abbreviatedOid": null,
"oid": null,
},
},
},
},
},
},
"headRefName": null,
"labels": Object {
"__args": Object {
"last": "100",
},
"nodes": Object {
"name": null,
},
},
"mergeStateStatus": null,
"mergeable": null,
"number": null,
"reviewRequests": Object {
"totalCount": null,
},
"reviews": Object {
"__args": Object {
"first": "1",
},
"nodes": Object {
"state": null,
},
},
"title": null,
},
"pageInfo": Object {
"endCursor": null,
"hasNextPage": null,
},
},
},
},
},
"headers": Object {
"accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "1504",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/graphql",
},
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"pullRequests": Object {
"__args": Object {
"first": "100",
},
"nodes": Object {
"comments": Object {
"__args": Object {
"last": "100",
},
"nodes": Object {
"body": null,
"databaseId": null,
},
},
"headRefName": null,
"number": null,
"state": null,
"title": null,
},
"pageInfo": Object {
"endCursor": null,
"hasNextPage": null,
},
},
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "604",
"content-type": "application/json",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://api.github.com/graphql",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "api.github.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://api.github.com/repos/some/repo/pulls/91",
},
]
`;
exports[`platform/github getBranchPr(branchName) should return null if no PR exists 1`] = ` exports[`platform/github getBranchPr(branchName) should return null if no PR exists 1`] = `
Array [ Array [
Object { Object {
@ -4199,70 +4593,6 @@ Array [
] ]
`; `;
exports[`platform/github massageMarkdown(input) returns not-updated pr body for GHE 1`] = `
Array [
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://github.company.com/user",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://github.company.com/user/emails",
},
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"defaultBranchRef": Object {
"name": null,
"target": Object {
"oid": null,
},
},
"isArchived": null,
"isFork": null,
"mergeCommitAllowed": null,
"nameWithOwner": null,
"rebaseMergeAllowed": null,
"squashMergeAllowed": null,
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "330",
"content-type": "application/json",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://github.company.com/graphql",
},
]
`;
exports[`platform/github massageMarkdown(input) returns updated pr body 1`] = `"https://github.com/foo/bar/issues/5 plus also [a link](https://togithub.com/foo/bar/issues/5)"`;
exports[`platform/github getRepoForceRebase should detect repoForceRebase 1`] = ` exports[`platform/github getRepoForceRebase should detect repoForceRebase 1`] = `
Array [ Array [
Object { Object {
@ -5335,6 +5665,70 @@ Array [
] ]
`; `;
exports[`platform/github massageMarkdown(input) returns not-updated pr body for GHE 1`] = `
Array [
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://github.company.com/user",
},
Object {
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "GET",
"url": "https://github.company.com/user/emails",
},
Object {
"graphql": Object {
"query": Object {
"repository": Object {
"__args": Object {
"name": "repo",
"owner": "some",
},
"defaultBranchRef": Object {
"name": null,
"target": Object {
"oid": null,
},
},
"isArchived": null,
"isFork": null,
"mergeCommitAllowed": null,
"nameWithOwner": null,
"rebaseMergeAllowed": null,
"squashMergeAllowed": null,
},
},
},
"headers": Object {
"accept": "application/vnd.github.v3+json",
"accept-encoding": "gzip, deflate",
"authorization": "token abc123",
"content-length": "330",
"content-type": "application/json",
"host": "github.company.com",
"user-agent": "https://github.com/renovatebot/renovate",
},
"method": "POST",
"url": "https://github.company.com/graphql",
},
]
`;
exports[`platform/github massageMarkdown(input) returns updated pr body 1`] = `"https://github.com/foo/bar/issues/5 plus also [a link](https://togithub.com/foo/bar/issues/5)"`;
exports[`platform/github mergePr(prNo) - autodetection should give up 1`] = ` exports[`platform/github mergePr(prNo) - autodetection should give up 1`] = `
Array [ Array [
Object { Object {

View file

@ -519,6 +519,101 @@ describe('platform/github', () => {
expect(pr).toMatchSnapshot(); expect(pr).toMatchSnapshot();
expect(httpMock.getTrace()).toMatchSnapshot(); expect(httpMock.getTrace()).toMatchSnapshot();
}); });
it('should reopen an autoclosed PR', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.post('/graphql')
.twice() // getOpenPrs() and getClosedPrs()
.reply(200, {
data: { repository: { pullRequests: { pageInfo: {} } } },
})
.get('/repos/some/repo/pulls?per_page=100&state=all')
.reply(200, [
{
number: 90,
head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
state: PrState.Open,
},
{
number: 91,
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
title: 'old title - autoclosed',
state: PrState.Closed,
},
])
.post('/repos/some/repo/git/refs')
.reply(201)
.patch('/repos/some/repo/pulls/91')
.reply(201)
.get('/repos/some/repo/pulls/91')
.reply(200, {
number: 91,
additions: 1,
deletions: 1,
commits: 1,
base: {
sha: '1234',
},
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
state: PrState.Open,
});
await github.initRepo({
repository: 'some/repo',
} as any);
const pr = await github.getBranchPr('somebranch');
expect(pr).toMatchSnapshot();
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('aborts reopening if branch recreation fails', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get('/repos/some/repo/pulls?per_page=100&state=all')
.reply(200, [
{
number: 91,
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
title: 'old title - autoclosed',
state: PrState.Closed,
},
])
.post('/repos/some/repo/git/refs')
.reply(201)
.patch('/repos/some/repo/pulls/91')
.reply(422);
await github.initRepo({
repository: 'some/repo',
} as any);
const pr = await github.getBranchPr('somebranch');
expect(pr).toBeNull();
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('aborts reopening if PR reopening fails', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope
.get('/repos/some/repo/pulls?per_page=100&state=all')
.reply(200, [
{
number: 91,
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
title: 'old title - autoclosed',
state: PrState.Closed,
},
])
.post('/repos/some/repo/git/refs')
.reply(422);
await github.initRepo({
repository: 'some/repo',
} as any);
const pr = await github.getBranchPr('somebranch');
expect(pr).toBeNull();
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('should return the PR object in fork mode', async () => { it('should return the PR object in fork mode', async () => {
const scope = httpMock.scope(githubApiHost); const scope = httpMock.scope(githubApiHost);
forkInitRepoMock(scope, 'some/repo', true); forkInitRepoMock(scope, 'some/repo', true);

View file

@ -791,11 +791,49 @@ export async function findPr({
// Returns the Pull Request for a branch. Null if not exists. // Returns the Pull Request for a branch. Null if not exists.
export async function getBranchPr(branchName: string): Promise<Pr | null> { export async function getBranchPr(branchName: string): Promise<Pr | null> {
logger.debug(`getBranchPr(${branchName})`); logger.debug(`getBranchPr(${branchName})`);
const existingPr = await findPr({ const openPr = await findPr({
branchName, branchName,
state: PrState.Open, state: PrState.Open,
}); });
return existingPr ? getPr(existingPr.number) : null; if (openPr) {
return getPr(openPr.number);
}
const autoclosedPr = await findPr({
branchName,
state: PrState.Closed,
});
if (autoclosedPr?.title?.endsWith(' - autoclosed')) {
logger.debug({ autoclosedPr }, 'Found autoclosed PR for branch');
const { sha, number } = autoclosedPr;
try {
await githubApi.postJson(`repos/${config.repository}/git/refs`, {
body: { ref: `refs/heads/${branchName}`, sha },
});
logger.debug({ branchName, sha }, 'Recreated autoclosed branch');
} catch (err) {
logger.debug('Could not recreate autoclosed branch - skipping reopen');
return null;
}
try {
const title = autoclosedPr.title.replace(/ - autoclosed$/, '');
await githubApi.patchJson(`repos/${config.repository}/pulls/${number}`, {
body: {
state: 'open',
title,
},
});
logger.info(
{ branchName, title, number },
'Successfully reopened autoclosed PR'
);
} catch (err) {
logger.debug('Could not reopen autoclosed PR');
return null;
}
delete config.closedPrList?.[number]; // So that it's no longer found in the closed list
return getPr(number);
}
return null;
} }
async function getStatus( async function getStatus(