From dc038b39621704c5aa4f1ad49b7bec3d8c77b37e Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Tue, 11 Sep 2018 06:07:50 +0200 Subject: [PATCH] refactor(github): use graphql to retrieve open pr list --- lib/platform/github/index.js | 123 ++++++++++++++ .../github/graphql/pullrequest-1.json | 158 ++++++++++++++++++ .../github/__snapshots__/index.spec.js.snap | 14 ++ test/platform/github/index.spec.js | 27 +++ 4 files changed, 322 insertions(+) create mode 100644 test/_fixtures/github/graphql/pullrequest-1.json diff --git a/lib/platform/github/index.js b/lib/platform/github/index.js index 5707c3906c..07d162a2b2 100644 --- a/lib/platform/github/index.js +++ b/lib/platform/github/index.js @@ -828,11 +828,134 @@ async function createPr( return pr; } +let openPrList; + +async function getOpenPrs() { + if (!openPrList) { + openPrList = {}; + try { + const url = 'graphql'; + // https://developer.github.com/v4/previews/#mergeinfopreview---more-detailed-information-about-a-pull-requests-merge-state + const headers = { + accept: 'application/vnd.github.merge-info-preview+json', + }; + // prettier-ignore + const query = ` + query { + repository(owner: "${config.repositoryOwner}", name: "${config.repositoryName}") { + pullRequests(states: [OPEN], first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + number + headRefName + title + mergeable + mergeStateStatus + commits(first: 2) { + nodes { + commit { + author { + email + } + committer { + email + } + parents(last: 1) { + edges { + node { + abbreviatedOid + oid + } + } + } + } + } + } + body + } + } + } + } + `; + const options = { + headers, + body: JSON.stringify({ query }), + json: false, + }; + const res = JSON.parse((await get.post(url, options)).body); + for (const pr of res.data.repository.pullRequests.nodes) { + // https://developer.github.com/v4/object/pullrequest/ + pr.displayNumber = `Pull Request #${pr.number}`; + pr.state = 'open'; + pr.branchName = pr.headRefName; + delete pr.headRefName; + // https://developer.github.com/v4/enum/mergeablestate + const canMergeStates = ['BEHIND', 'CLEAN']; + if (canMergeStates.includes(pr.mergeStateStatus)) { + pr.canMerge = true; + } else { + pr.canMerge = false; + } + // https://developer.github.com/v4/enum/mergestatestatus + if (pr.mergeStateStatus === 'DIRTY') { + pr.isUnmergeable = true; + } else { + pr.isUnmergeable = false; + } + if (pr.commits.nodes.length === 1) { + if (config.gitAuthor) { + // Check against gitAuthor + const commitAuthorEmail = pr.commits.nodes[0].commit.author.email; + if (commitAuthorEmail === config.gitAuthor.address) { + pr.canRebase = true; + } else { + pr.canRebase = false; + } + } else { + // assume the author is us + // istanbul ignore next + pr.canRebase = true; + } + } else { + // assume we can't rebase if more than 1 + pr.canRebase = false; + } + pr.isStale = false; + if (pr.mergeStateStatus === 'BEHIND') { + pr.isStale = true; + } else { + const baseCommitSHA = await getBaseCommitSHA(); + if ( + pr.commits.nodes[0].commit.parents.edges[0].node.oid !== + baseCommitSHA + ) { + pr.isStale = true; + } + } + delete pr.mergeable; + delete pr.mergeStateStatus; + delete pr.commits; + openPrList[pr.number] = pr; + } + } catch (err) /* istanbul ignore next */ { + logger.warn({ err }, 'getOpenPrs error'); + } + } + return openPrList; +} + // Gets details for a PR async function getPr(prNo) { if (!prNo) { return null; } + const openPr = (await getOpenPrs())[prNo]; + if (openPr) { + return openPr; + } + logger.info( + { prNo }, + 'PR not found in open PRs list - trying to fetch it directly' + ); const pr = (await get( `repos/${config.parentRepo || config.repository}/pulls/${prNo}` )).body; diff --git a/test/_fixtures/github/graphql/pullrequest-1.json b/test/_fixtures/github/graphql/pullrequest-1.json new file mode 100644 index 0000000000..c17fdd04b6 --- /dev/null +++ b/test/_fixtures/github/graphql/pullrequest-1.json @@ -0,0 +1,158 @@ +{ + "data": { + "repository": { + "pullRequests": { + "nodes": [ + { + "number": 2433, + "headRefName": "renovate/major-got-packages", + "title": "build(deps): update got packages (major)", + "mergeable": "MERGEABLE", + "mergeStateStatus": "CLEAN", + "commits": { + "nodes": [ + { + "commit": { + "author": { + "email": "bot@renovateapp.com" + }, + "committer": { + "name": "Renovate Bot", + "email": "bot@renovateapp.com" + }, + "parents": { + "edges": [ + { + "node": { + "abbreviatedOid": "1234", + "oid": "1234123412341234123412341234123412341234" + } + } + ] + } + } + } + ] + } + }, + { + "number": 2500, + "headRefName": "renovate/jest-monorepo", + "title": "chore(deps): update dependency jest to v23.6.0", + "mergeable": "UNKNOWN", + "mergeStateStatus": "DIRTY", + "commits": { + "nodes": [ + { + "commit": { + "author": { + "email": "bot@renovateapp.com" + }, + "committer": { + "name": "Renovate Bot", + "email": "bot@renovateapp.com" + }, + "parents": { + "edges": [ + { + "node": { + "abbreviatedOid": "b08d1aa", + "oid": "b08d1aa8150c31516dcdf1d50a30020612e65d04" + } + } + ] + } + } + } + ] + } + }, + { + "number": 2079, + "headRefName": "feat/nodever", + "title": "feat: node versioning (WIP)", + "mergeable": "MERGEABLE", + "commits": { + "nodes": [ + { + "commit": { + "author": { + "email": "rhys@arkins.net" + }, + "committer": { + "name": "Rhys Arkins", + "email": "rhys@arkins.net" + }, + "parents": { + "edges": [ + { + "node": { + "abbreviatedOid": "233fa20", + "oid": "233fa2078104581fba6beac10f3fd6765dedb300" + } + } + ] + } + } + } + ] + } + }, + { + "number": 2086, + "headRefName": "fix/deletePRafterDeleteBranch", + "title": "feat(vsts): abandon pr after delete branch", + "mergeable": "MERGEABLE", + "mergeStateStatus": "BEHIND", + "commits": { + "nodes": [ + { + "commit": { + "author": { + "email": "SESA8879@schneider-electric.com" + }, + "committer": { + "name": "Jean-Yves COUET", + "email": "SESA8879@schneider-electric.com" + }, + "parents": { + "edges": [ + { + "node": { + "abbreviatedOid": "670cfd8", + "oid": "670cfd8feeda9d6236caeb8afe5d60191a275bc6" + } + } + ] + } + } + }, + { + "commit": { + "author": { + "email": "SESA8879@schneider-electric.com" + }, + "committer": { + "name": "Jean-Yves COUET", + "email": "SESA8879@schneider-electric.com" + }, + "parents": { + "edges": [ + { + "node": { + "abbreviatedOid": "c696654", + "oid": "c69665439d8c1bf25cafc9c2db8e87c7ab274714" + } + } + ] + } + } + } + ] + } + } + ] + } + } + } +} diff --git a/test/platform/github/__snapshots__/index.spec.js.snap b/test/platform/github/__snapshots__/index.spec.js.snap index a2978b99ac..52426c22dc 100644 --- a/test/platform/github/__snapshots__/index.spec.js.snap +++ b/test/platform/github/__snapshots__/index.spec.js.snap @@ -270,6 +270,20 @@ Array [ ] `; +exports[`platform/github getPr(prNo) should return PR from graphql result 1`] = ` +Object { + "branchName": "renovate/jest-monorepo", + "canMerge": false, + "canRebase": true, + "displayNumber": "Pull Request #2500", + "isStale": true, + "isUnmergeable": true, + "number": 2500, + "state": "open", + "title": "chore(deps): update dependency jest to v23.6.0", +} +`; + exports[`platform/github getPr(prNo) should return a PR object - 0 1`] = ` Object { "base": Object { diff --git a/test/platform/github/index.spec.js b/test/platform/github/index.spec.js index e83b28d724..c48f243cd4 100644 --- a/test/platform/github/index.spec.js +++ b/test/platform/github/index.spec.js @@ -1,3 +1,5 @@ +const fs = require('fs-extra'); + describe('platform/github', () => { let github; let get; @@ -1124,6 +1126,31 @@ describe('platform/github', () => { const pr = await github.getPr(null); expect(pr).toBe(null); }); + it('should return PR from graphql result', async () => { + await initRepo({ + repository: 'some/repo', + token: 'token', + gitAuthor: 'bot@renovateapp.com', + }); + const res1 = fs.readFileSync( + 'test/_fixtures/github/graphql/pullrequest-1.json', + 'utf8' + ); + get.post.mockImplementationOnce(() => ({ + body: res1, + })); + // getBranchCommit + get.mockImplementationOnce(() => ({ + body: { + object: { + sha: '1234123412341234123412341234123412341234', + }, + }, + })); + const pr = await github.getPr(2500); + expect(pr).toBeDefined(); + expect(pr).toMatchSnapshot(); + }); it('should return null if no PR is returned from GitHub', async () => { await initRepo({ repository: 'some/repo', token: 'token' }); get.mockImplementationOnce(() => ({ -- GitLab