diff --git a/lib/success.js b/lib/success.js index 48246f29..02d463e6 100644 --- a/lib/success.js +++ b/lib/success.js @@ -76,7 +76,7 @@ export default async function success(pluginConfig, context, { Octokit }) { const shas = commits.map(({ hash }) => hash); // Get associatedPRs - const associatedPRs = await inChunks(shas, 100, async (chunk) => { + const associatedPRs = await inChunks(shas, 25, async (chunk) => { const responsePRs = []; const { repository } = await octokit.graphql( buildAssociatedPRsQuery(chunk), @@ -333,8 +333,8 @@ export default async function success(pluginConfig, context, { Octokit }) { } /** - * In order to speed up a function call that handles a big array of items, we split up the - * array in chunks and call the function for each chunk in parallel. At the end we combine the + * Split a big array of items in chunks and call the function for each chunk serially, so that + * large releases do not send many heavy GraphQL queries at once. At the end we combine the * results again. * * @template TItem @@ -345,11 +345,10 @@ export default async function success(pluginConfig, context, { Octokit }) { * @returns TCallbackResult */ async function inChunks(items, chunkSize, callback) { - const chunkCalls = []; + const results = []; for (let i = 0; i < items.length; i += chunkSize) { - chunkCalls.push(callback(items.slice(i, i + chunkSize))); + results.push(await callback(items.slice(i, i + chunkSize))); } - const results = await Promise.all(chunkCalls); return results.flat(); } diff --git a/test/success.test.js b/test/success.test.js index e88e1b70..2478ed71 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -1387,6 +1387,83 @@ test("Do not add comment and labels if no PR is associated with release commits" t.true(fetch.done()); }); +test("Query associated PRs in serial chunks of 25 commits", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const options = { + branch: "master", + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = Array.from({ length: 30 }, (_, index) => ({ + hash: `${index}`, + message: `Commit ${index} message`, + })); + const nextRelease = { version: "1.0.0" }; + const releases = [ + { name: "GitHub release", url: "https://github.com/release" }, + ]; + const chunkSizes = []; + let inFlight = 0; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }) + .post( + (url, { body }) => + JSON.parse(body).query.includes("query getAssociatedPRs("), + async (url, { body }) => { + inFlight += 1; + t.is(inFlight, 1); + const shas = [...JSON.parse(body).query.matchAll(/oid: "(\d+)"/g)].map( + ([, sha]) => sha, + ); + chunkSizes.push(shas.length); + await new Promise((resolve) => setTimeout(resolve, 10)); + inFlight -= 1; + const nodes = { pageInfo: { hasNextPage: false }, nodes: [] }; + return { + data: { + repository: Object.fromEntries( + shas.map((sha) => [ + `commit${sha}`, + { oid: sha, associatedPullRequests: nodes }, + ]), + ), + }, + }; + }, + ) + .postOnce( + (url, { body }) => JSON.parse(body).query.includes("query getSRIssues("), + { data: { repository: { issues: { nodes: [] } } } }, + ); + + await success( + {}, + { + env, + options, + commits, + nextRelease, + releases, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.deepEqual(chunkSizes, [25, 5]); + t.true(fetch.done()); +}); + test("Do not add comment and labels if no commits is found for release", async (t) => { const owner = "test_user"; const repo = "test_repo";