diff --git a/lib/platform/github/__snapshots__/index.spec.ts.snap b/lib/platform/github/__snapshots__/index.spec.ts.snap index 364f976566650714cfe22337132cc5f573a28ce0..ea29ddd14c88dc69c5219ebbe2501554d6dd64ad 100644 --- a/lib/platform/github/__snapshots__/index.spec.ts.snap +++ b/lib/platform/github/__snapshots__/index.spec.ts.snap @@ -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`] = ` Array [ 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`] = ` Array [ 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`] = ` Array [ Object { diff --git a/lib/platform/github/index.spec.ts b/lib/platform/github/index.spec.ts index 0199d5cfe8091691a4ffa815065d959214d82b8a..d85d0c818fed4b0b312386aff0f8ca05ff1cc4f8 100644 --- a/lib/platform/github/index.spec.ts +++ b/lib/platform/github/index.spec.ts @@ -519,6 +519,101 @@ describe('platform/github', () => { expect(pr).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 () => { const scope = httpMock.scope(githubApiHost); forkInitRepoMock(scope, 'some/repo', true); diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts index 7c48180185f8e2733f8f0b5b6e0799b6f733be21..247cc1389d3dbcdf4ce67ed871476149fb6cccb5 100644 --- a/lib/platform/github/index.ts +++ b/lib/platform/github/index.ts @@ -791,11 +791,49 @@ export async function findPr({ // Returns the Pull Request for a branch. Null if not exists. export async function getBranchPr(branchName: string): Promise<Pr | null> { logger.debug(`getBranchPr(${branchName})`); - const existingPr = await findPr({ + const openPr = await findPr({ branchName, 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(