From d4e7cf65f39f34eccead3f164c64ce75c7c71ad4 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 18:49:05 +0100 Subject: [PATCH 1/8] refactor: stop export of `buildAssociatedPRsQuery` --- lib/success.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index 8c2d4583..93f28794 100644 --- a/lib/success.js +++ b/lib/success.js @@ -252,7 +252,7 @@ export default async function success(pluginConfig, context, { Octokit }) { * @param {Array} shas * @returns {string} */ -export function buildAssociatedPRsQuery(shas) { +function buildAssociatedPRsQuery(shas) { return `#graphql query getAssociatedPRs($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { @@ -275,3 +275,4 @@ export function buildAssociatedPRsQuery(shas) { } `; } + From 5a829c2d11c065311193bd18bd819adcc463535e Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 18:52:28 +0100 Subject: [PATCH 2/8] feat: add `pageInfo` to `getAssociatedPRs` query --- lib/success.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/success.js b/lib/success.js index 93f28794..d23edb0e 100644 --- a/lib/success.js +++ b/lib/success.js @@ -261,6 +261,10 @@ function buildAssociatedPRsQuery(shas) { return `commit${sha.slice(0, 6)}: object(oid: "${sha}") { ...on Commit { associatedPullRequests(first: 100) { + pageInfo { + endCursor + hasNextPage + } nodes { url number From 2606a53d32d27daaa994298474df4197bb995817 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 19:19:56 +0100 Subject: [PATCH 3/8] feat: add `loadSingleCommitAssociatedPRs` graphql query --- lib/success.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/success.js b/lib/success.js index d23edb0e..bcf75590 100644 --- a/lib/success.js +++ b/lib/success.js @@ -280,3 +280,27 @@ function buildAssociatedPRsQuery(shas) { `; } +/** + * GraphQL Query to fetch additional associatedPR for commits that has more than 100 associatedPRs + */ +const loadSingleCommitAssociatedPRs = `#graphql + query getCommitAssociatedPRs($owner: String!, $repo: String!, $sha: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + commit: object(oid: $sha) { + ...on Commit { + associatedPullRequests(after: $cursor, first: 100) { + pageInfo { + endCursor + hasNextPage + } + nodes { + url + number + body + } + } + } + } + } + } +`; \ No newline at end of file From 4f91b4958057d69edc70c214cecc267f12da4784 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 20:45:50 +0100 Subject: [PATCH 4/8] feat: implement logic to fetch more `associatedPRs` --- lib/success.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/success.js b/lib/success.js index bcf75590..0ea57b34 100644 --- a/lib/success.js +++ b/lib/success.js @@ -71,10 +71,33 @@ export default async function success(pluginConfig, context, { Octokit }) { buildAssociatedPRsQuery(shas), { owner, repo }, ); - const associatedPRs = Object.values(repository).map( - (item) => item.associatedPullRequests.nodes, + const responseAssociatedPRs = Object.values(repository).map( + (item) => item.associatedPullRequests, ); + const associatedPRs = []; + + for (const { nodes, pageInfo } of responseAssociatedPRs) { + associatedPRs.push(nodes); + if (pageInfo.hasNextPage) { + let cursor = pageInfo.endCursor; + let hasNextPage = true; + while (hasNextPage) { + const { repository } = await octokit.graphql( + loadSingleCommitAssociatedPRs, + { owner, repo, sha: response.commit.oid, cursor }, + ); + const { associatedPullRequests } = repository.commit; + associatedPRs.push(associatedPullRequests.nodes); + if (associatedPullRequests.pageInfo.hasNextPage) { + cursor = associatedPullRequests.pageInfo.endCursor; + } else { + hasNextPage = false; + } + } + } + } + const uniqueAssociatedPRs = uniqBy(flatten(associatedPRs), "number"); const prs = await pFilter(uniqueAssociatedPRs, async ({ number }) => { @@ -260,6 +283,7 @@ function buildAssociatedPRsQuery(shas) { .map((sha) => { return `commit${sha.slice(0, 6)}: object(oid: "${sha}") { ...on Commit { + oid associatedPullRequests(first: 100) { pageInfo { endCursor @@ -303,4 +327,4 @@ const loadSingleCommitAssociatedPRs = `#graphql } } } -`; \ No newline at end of file +`; From e893722f7ac6b45b4333dbb22056fc62a4768f6e Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 21:28:29 +0100 Subject: [PATCH 5/8] test: fix unit tests in `success` --- test/success.test.js | 165 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/test/success.test.js b/test/success.test.js index 8d87dccd..298c7f0e 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -62,12 +62,22 @@ test("Add comment and labels to PRs associated with release commits and issues s data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, @@ -232,12 +242,22 @@ test("Add comment and labels to PRs associated with release commits and issues c data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, @@ -423,32 +443,62 @@ test("Make multiple search queries if necessary", async (t) => { data: { repository: { commitaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: { + oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commitbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: { + oid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, commitcccccccccccccccccccccccccccccccccccccccccc: { + oid: "cccccccccccccccccccccccccccccccccccccccccc", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[2]], }, }, commiteeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee: { + oid: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[3]], }, }, commitffffffffffffffffffffffffffffffffffffffffff: { + oid: "ffffffffffffffffffffffffffffffffffffffffff", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[4]], }, }, commitgggggggggggggggggggggggggggggggggggggggggg: { + oid: "gggggggggggggggggggggggggggggggggggggggggg", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[5]], }, }, @@ -667,12 +717,22 @@ test("Do not add comment and labels for unrelated PR returned by search (compare data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, @@ -769,7 +829,12 @@ test("Do not add comment and labels if no PR is associated with release commits" data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, @@ -890,17 +955,32 @@ test("Do not add comment and labels to PR/issues from other repo", async (t) => data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, commit789: { + oid: "789", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, @@ -990,17 +1070,32 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, commit789: { + oid: "789", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[2]], }, }, @@ -1159,7 +1254,12 @@ test("Add custom comment and labels", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1254,7 +1354,12 @@ test("Add custom label", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1344,7 +1449,12 @@ test("Comment on issue/PR without ading a label", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1437,7 +1547,12 @@ test("Editing the release to include all release links at the bottom", async (t) data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1541,7 +1656,12 @@ test("Editing the release to include all release links at the top", async (t) => data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1642,7 +1762,12 @@ test("Editing the release to include all release links with no additional releas data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1732,7 +1857,12 @@ test("Editing the release to include all release links with no additional releas data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1815,7 +1945,12 @@ test("Editing the release to include all release links with no releases", async data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1900,7 +2035,12 @@ test("Editing the release with no ID in the release", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -1990,12 +2130,22 @@ test("Ignore errors when adding comments and closing issues", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, commit456: { + oid: "456", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[1]], }, }, @@ -2123,7 +2273,12 @@ test("Close open issues when a release is successful", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, @@ -2274,7 +2429,12 @@ test('Skip closing issues if "failComment" is "false"', async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, @@ -2325,7 +2485,12 @@ test('Skip closing issues if "failTitle" is "false"', async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [], }, }, From aa68ac864ee8633684a5eb20bee4516c01609f46 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 2 Aug 2024 21:28:52 +0100 Subject: [PATCH 6/8] test: fix `intergration` --- test/integration.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/integration.test.js b/test/integration.test.js index b9f2f299..1dc9a221 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -436,7 +436,12 @@ test("Comment and add labels on PR included in the releases", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -656,7 +661,12 @@ test("Verify, release and notify success", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, @@ -809,7 +819,12 @@ test("Verify, update release and notify success", async (t) => { data: { repository: { commit123: { + oid: "123", associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, nodes: [prs[0]], }, }, From bf905186f7bd17b2782c04800bc9b8292826c1ee Mon Sep 17 00:00:00 2001 From: babblebey Date: Sun, 4 Aug 2024 15:03:19 +0100 Subject: [PATCH 7/8] feat: implement logic to handle associatedPRs fetch on 100+ commits --- lib/success.js | 56 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/lib/success.js b/lib/success.js index 0ea57b34..59ab8d0f 100644 --- a/lib/success.js +++ b/lib/success.js @@ -67,32 +67,40 @@ export default async function success(pluginConfig, context, { Octokit }) { const releaseInfos = releases.filter((release) => Boolean(release.name)); const shas = commits.map(({ hash }) => hash); - const { repository } = await octokit.graphql( - buildAssociatedPRsQuery(shas), - { owner, repo }, - ); - const responseAssociatedPRs = Object.values(repository).map( - (item) => item.associatedPullRequests, - ); - const associatedPRs = []; - for (const { nodes, pageInfo } of responseAssociatedPRs) { - associatedPRs.push(nodes); - if (pageInfo.hasNextPage) { - let cursor = pageInfo.endCursor; - let hasNextPage = true; - while (hasNextPage) { - const { repository } = await octokit.graphql( - loadSingleCommitAssociatedPRs, - { owner, repo, sha: response.commit.oid, cursor }, - ); - const { associatedPullRequests } = repository.commit; - associatedPRs.push(associatedPullRequests.nodes); - if (associatedPullRequests.pageInfo.hasNextPage) { - cursor = associatedPullRequests.pageInfo.endCursor; - } else { - hasNextPage = false; + // Split commit shas into chunks of 100 shas + const chunkSize = 100; + const shasChunks = []; + for (let i = 0; i < shas.length; i += chunkSize) { + const chunk = shas.slice(i, i + chunkSize); + shasChunks.push(chunk); + } + for (const chunk of shasChunks) { + const { repository } = await octokit.graphql( + buildAssociatedPRsQuery(chunk), + { owner, repo }, + ); + const responseAssociatedPRs = Object.values(repository).map( + (item) => item.associatedPullRequests, + ); + for (const { nodes, pageInfo } of responseAssociatedPRs) { + associatedPRs.push(nodes); + if (pageInfo.hasNextPage) { + let cursor = pageInfo.endCursor; + let hasNextPage = true; + while (hasNextPage) { + const { repository } = await octokit.graphql( + loadSingleCommitAssociatedPRs, + { owner, repo, sha: response.commit.oid, cursor }, + ); + const { associatedPullRequests } = repository.commit; + associatedPRs.push(associatedPullRequests.nodes); + if (associatedPullRequests.pageInfo.hasNextPage) { + cursor = associatedPullRequests.pageInfo.endCursor; + } else { + hasNextPage = false; + } } } } From ad7342dcfab33ecf501c4509ab1d4a34f1e637b9 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 9 Aug 2024 17:09:23 +0100 Subject: [PATCH 8/8] test: add test case `dd comment and labels to PRs associated with release commits and issues (multipaged associatedPRs)` --- test/success.test.js | 191 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/test/success.test.js b/test/success.test.js index 298c7f0e..410609eb 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -202,6 +202,197 @@ test("Add comment and labels to PRs associated with release commits and issues s t.true(fetch.done()); }); +test("Add comment and labels to PRs associated with release commits and issues (multipaged associatedPRs)", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const failTitle = "The automated release is failing 🚨"; + const pluginConfig = { failTitle }; + const prs = [ + { number: 1, pull_request: {}, state: "closed" }, + { number: 2, pull_request: {}, body: "Fixes #3", state: "closed" }, + { number: 5, pull_request: {}, state: "closed" }, + { number: 6, pull_request: {}, state: "closed" }, + ]; + const options = { + branch: "master", + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = [ + { + hash: "123", + message: "Commit 1 message\n\n Fix #1", + tree: { long: "aaa" }, + }, + { hash: "456", message: "Commit 2 message", tree: { long: "ccc" } }, + { + hash: "789", + message: `Commit 3 message Closes https://github.com/${owner}/${repo}/issues/4`, + tree: { long: "ccc" }, + }, + ]; + const nextRelease = { version: "1.0.0" }; + const releases = [ + { name: "GitHub release", url: "https://github.com/release" }, + ]; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + full_name: `${owner}/${repo}`, + }) + .postOnce("https://api.github.local/graphql", { + data: { + repository: { + commit123: { + oid: "123", + associatedPullRequests: { + pageInfo: { + endCursor: "YE", + hasNextPage: true, + }, + nodes: [prs[0]], + }, + }, + commit456: { + oid: "456", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[1]], + }, + }, + commit789: { + oid: "789", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[2]], + }, + }, + }, + }, + }) + .postOnce( + "https://api.github.local/graphql", + { + data: { + repository: { + commit: { + associatedPullRequests: { + pageInfo: { + endCursor: "NE", + hasNextPage: false, + }, + nodes: [prs[3]], + }, + }, + }, + }, + }, + { + overwriteRoutes: true, + }, + ) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/6/commits`, + [{ sha: commits[0].hash }], + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/1/comments`, + { + html_url: "https://github.com/successcomment-1", + }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/1/labels`, + {}, + { body: ["released"] }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/4/comments`, + { html_url: "https://github.com/successcomment-4" }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/4/labels`, + {}, + { body: ["released"] }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/6/comments`, + { html_url: "https://github.com/successcomment-6" }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/6/labels`, + {}, + { body: ["released"] }, + ) + .getOnce( + `https://api.github.local/search/issues?q=${encodeURIComponent( + "in:title", + )}+${encodeURIComponent( + `repo:${owner}/${repo}`, + )}+${encodeURIComponent("type:issue")}+${encodeURIComponent( + "state:open", + )}+${encodeURIComponent(failTitle)}`, + { items: [] }, + ); + + await success( + pluginConfig, + { + env, + options, + commits, + nextRelease, + releases, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true( + t.context.log.calledWith( + "Added comment to issue #%d: %s", + 1, + "https://github.com/successcomment-1", + ), + ); + t.true( + t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + ); + t.true( + t.context.log.calledWith( + "Added comment to issue #%d: %s", + 4, + "https://github.com/successcomment-4", + ), + ); + t.true( + t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 4), + ); + t.true( + t.context.log.calledWith( + "Added comment to issue #%d: %s", + 6, + "https://github.com/successcomment-6", + ), + ); + t.true( + t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 6), + ); + t.true(fetch.done()); +}); + test("Add comment and labels to PRs associated with release commits and issues closed by PR/commits comments with custom URL", async (t) => { const owner = "test_user"; const repo = "test_repo";