From 304d0230aaf45e34ccd246844528bd35fb95b337 Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 10 Jul 2024 19:17:31 +0100 Subject: [PATCH 01/58] feat: add `failCommentCondition` to `fail` script --- lib/fail.js | 63 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/fail.js b/lib/fail.js index 9439b87a..4552b831 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -23,14 +23,19 @@ export default async function fail(pluginConfig, context, { Octokit }) { githubApiPathPrefix, githubApiUrl, proxy, - failComment, failTitle, + failComment, + failCommentCondition, labels, assignees, } = resolveConfig(pluginConfig, context); if (failComment === false || failTitle === false) { logger.log("Skip issue creation."); + logger.error(`Failure reporting should be disabled via 'failCommentCondition'. + Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`); + } else if (failCommentCondition === false) { + logger.log("Skip issue creation."); } else { const octokit = new Octokit( toOctokitOptions({ @@ -52,31 +57,39 @@ export default async function fail(pluginConfig, context, { Octokit }) { : getFailComment(branch, errors); const [srIssue] = await findSRIssues(octokit, failTitle, owner, repo); - if (srIssue) { - logger.log("Found existing semantic-release issue #%d.", srIssue.number); - const comment = { owner, repo, issue_number: srIssue.number, body }; - debug("create comment: %O", comment); - const { - data: { html_url: url }, - } = await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - comment, - ); - logger.log("Added comment to issue #%d: %s.", srIssue.number, url); + const canCommentOnOrCreateIssue = failCommentCondition + ? template(failCommentCondition)({...context, issue: srIssue}) + : true; + + if (canCommentOnOrCreateIssue) { + if (srIssue) { + logger.log("Found existing semantic-release issue #%d.", srIssue.number); + const comment = { owner, repo, issue_number: srIssue.number, body }; + debug("create comment: %O", comment); + const { + data: { html_url: url }, + } = await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + comment, + ); + logger.log("Added comment to issue #%d: %s.", srIssue.number, url); + } else { + const newIssue = { + owner, + repo, + title: failTitle, + body: `${body}\n\n${ISSUE_ID}`, + labels: labels || [], + assignees, + }; + debug("create issue: %O", newIssue); + const { + data: { html_url: url, number }, + } = await octokit.request("POST /repos/{owner}/{repo}/issues", newIssue); + logger.log("Created issue #%d: %s.", number, url); + } } else { - const newIssue = { - owner, - repo, - title: failTitle, - body: `${body}\n\n${ISSUE_ID}`, - labels: labels || [], - assignees, - }; - debug("create issue: %O", newIssue); - const { - data: { html_url: url, number }, - } = await octokit.request("POST /repos/{owner}/{repo}/issues", newIssue); - logger.log("Created issue #%d: %s.", number, url); + logger.log("Skip commenting on or creating an issue."); } } } From 120d391c523889cbd0afc53f7fe7126fb53617ca Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 10 Jul 2024 19:18:15 +0100 Subject: [PATCH 02/58] feat: add `successCommentCondition` and `failCommentCondition` to `resolve-config` --- lib/resolve-config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/resolve-config.js b/lib/resolve-config.js index b8081c88..578a945e 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -8,8 +8,10 @@ export default function resolveConfig( proxy, assets, successComment, + successCommentCondition, failTitle, failComment, + failCommentCondition, labels, assignees, releasedLabels, @@ -30,10 +32,12 @@ export default function resolveConfig( proxy: isNil(proxy) ? env.http_proxy || env.HTTP_PROXY || false : proxy, assets: assets ? castArray(assets) : assets, successComment, + successCommentCondition, failTitle: isNil(failTitle) ? "The automated release is failing 🚨" : failTitle, failComment, + failCommentCondition, labels: isNil(labels) ? ["semantic-release"] : labels === false From ec8f4d61e15563bd642373fa90e8d70663c2b432 Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 10 Jul 2024 19:26:52 +0100 Subject: [PATCH 03/58] feat: add `successCommentCondition` and `failCommentCondition` to `success` scipt --- lib/success.js | 108 ++++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/lib/success.js b/lib/success.js index 49ad22e1..820ecb68 100644 --- a/lib/success.js +++ b/lib/success.js @@ -29,8 +29,10 @@ export default async function success(pluginConfig, context, { Octokit }) { githubApiUrl, proxy, successComment, - failComment, + successCommentCondition, failTitle, + failComment, + failCommentCondition, releasedLabels, addReleases, } = resolveConfig(pluginConfig, context); @@ -56,6 +58,10 @@ export default async function success(pluginConfig, context, { Octokit }) { if (successComment === false) { logger.log("Skip commenting on issues and pull requests."); + logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'. + Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`); + } else if (successCommentCondition === false) { + logger.log("Skip commenting on issues and pull requests."); } else { const parser = issueParser( "github", @@ -127,54 +133,62 @@ export default async function success(pluginConfig, context, { Octokit }) { await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { - const body = successComment - ? template(successComment)({ ...context, issue }) - : getSuccessComment(issue, releaseInfos, nextRelease); - try { - const comment = { owner, repo, issue_number: issue.number, body }; - debug("create comment: %O", comment); - const { - data: { html_url: url }, - } = await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - comment, - ); - logger.log("Added comment to issue #%d: %s", issue.number, url); - - if (releasedLabels) { - const labels = releasedLabels.map((label) => - template(label)(context), + const canCommentOnIssuesAndPRs = successCommentCondition + ? template(successCommentCondition)({ ...context, issue }) + : true; + + if (canCommentOnIssuesAndPRs) { + const body = successComment + ? template(successComment)({ ...context, issue }) + : getSuccessComment(issue, releaseInfos, nextRelease); + try { + const comment = { owner, repo, issue_number: issue.number, body }; + debug("create comment: %O", comment); + const { + data: { html_url: url }, + } = await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + comment, ); - await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/labels", - { - owner, - repo, - issue_number: issue.number, - data: labels, - }, - ); - logger.log("Added labels %O to issue #%d", labels, issue.number); - } - } catch (error) { - if (error.status === 403) { - logger.error( - "Not allowed to add a comment to the issue #%d.", - issue.number, - ); - } else if (error.status === 404) { - logger.error( - "Failed to add a comment to the issue #%d as it doesn't exist.", - issue.number, - ); - } else { - errors.push(error); - logger.error( - "Failed to add a comment to the issue #%d.", - issue.number, - ); - // Don't throw right away and continue to update other issues + logger.log("Added comment to issue #%d: %s", issue.number, url); + + if (releasedLabels) { + const labels = releasedLabels.map((label) => + template(label)(context), + ); + await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/labels", + { + owner, + repo, + issue_number: issue.number, + data: labels, + }, + ); + logger.log("Added labels %O to issue #%d", labels, issue.number); + } + } catch (error) { + if (error.status === 403) { + logger.error( + "Not allowed to add a comment to the issue #%d.", + issue.number, + ); + } else if (error.status === 404) { + logger.error( + "Failed to add a comment to the issue #%d as it doesn't exist.", + issue.number, + ); + } else { + errors.push(error); + logger.error( + "Failed to add a comment to the issue #%d.", + issue.number, + ); + // Don't throw right away and continue to update other issues + } } + } else { + logger.log("Skip commenting on issue #%d.", issue.id); } }), ); From 508e066a29ab91bdce1d8b655a6df8ca4405dfae Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 10 Jul 2024 19:31:27 +0100 Subject: [PATCH 04/58] fix(build): lint --- lib/fail.js | 14 ++++++++++---- lib/success.js | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/fail.js b/lib/fail.js index 4552b831..2e3d950e 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -57,13 +57,16 @@ export default async function fail(pluginConfig, context, { Octokit }) { : getFailComment(branch, errors); const [srIssue] = await findSRIssues(octokit, failTitle, owner, repo); - const canCommentOnOrCreateIssue = failCommentCondition - ? template(failCommentCondition)({...context, issue: srIssue}) + const canCommentOnOrCreateIssue = failCommentCondition + ? template(failCommentCondition)({ ...context, issue: srIssue }) : true; if (canCommentOnOrCreateIssue) { if (srIssue) { - logger.log("Found existing semantic-release issue #%d.", srIssue.number); + logger.log( + "Found existing semantic-release issue #%d.", + srIssue.number, + ); const comment = { owner, repo, issue_number: srIssue.number, body }; debug("create comment: %O", comment); const { @@ -85,7 +88,10 @@ export default async function fail(pluginConfig, context, { Octokit }) { debug("create issue: %O", newIssue); const { data: { html_url: url, number }, - } = await octokit.request("POST /repos/{owner}/{repo}/issues", newIssue); + } = await octokit.request( + "POST /repos/{owner}/{repo}/issues", + newIssue, + ); logger.log("Created issue #%d: %s.", number, url); } } else { diff --git a/lib/success.js b/lib/success.js index 820ecb68..89b46e4d 100644 --- a/lib/success.js +++ b/lib/success.js @@ -133,11 +133,11 @@ export default async function success(pluginConfig, context, { Octokit }) { await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { - const canCommentOnIssuesAndPRs = successCommentCondition + const canCommentOnIssuesAndPRs = successCommentCondition ? template(successCommentCondition)({ ...context, issue }) : true; - - if (canCommentOnIssuesAndPRs) { + + if (canCommentOnIssuesAndPRs) { const body = successComment ? template(successComment)({ ...context, issue }) : getSuccessComment(issue, releaseInfos, nextRelease); From f3198962685cd91c515874ed746df4d19d87e97a Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 12 Jul 2024 19:01:13 +0100 Subject: [PATCH 05/58] test: add `fail` case `Does not post comments if "failCommentCondition" is "false"` --- test/fail.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/fail.test.js b/test/fail.test.js index deb8b79d..0b5bbd61 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -447,3 +447,29 @@ test('Skip if "failTitle" is "false"', async (t) => { t.true(t.context.log.calledWith("Skip issue creation.")); }); + +test('Does not post comments if "failCommentCondition" is "false"', async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const pluginConfig = { failCommentCondition: false }; + const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; + + await fail( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true(t.context.log.calledWith("Skip issue creation.")); +}); From 5983b75a1255e327b3d7b443ffc18be7ca67068c Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 12 Jul 2024 21:44:57 +0100 Subject: [PATCH 06/58] test: add `fail` case for `Does not post comments on existing issues when "failCommentCondition" is "false"` --- test/fail.test.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/fail.test.js b/test/fail.test.js index 0b5bbd61..dd3b5610 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -473,3 +473,56 @@ test('Does not post comments if "failCommentCondition" is "false"', async (t) => t.true(t.context.log.calledWith("Skip issue creation.")); }); + +test('Does not post comments on existing issues when "failCommentCondition" is "false"', async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const failTitle = "The automated release is failing 🚨"; + const env = { GITHUB_TOKEN: "github_token" }; + const pluginConfig = { failCommentCondition: "<% return false; %>" }; + const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; + const errors = [ + new SemanticReleaseError("Error message 1", "ERR1", "Error 1 details"), + new SemanticReleaseError("Error message 2", "ERR2", "Error 2 details"), + new SemanticReleaseError("Error message 3", "ERR3", "Error 3 details"), + ]; + const issues = [ + { number: 1, body: "Issue 1 body", title: failTitle }, + { number: 2, body: `Issue 2 body\n\n${ISSUE_ID}`, title: failTitle }, + { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, + ]; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + full_name: `${owner}/${repo}`, + }) + .getOnce( + `https://api.github.local/search/issues?q=${encodeURIComponent( + "in:title", + )}+${encodeURIComponent(`repo:${owner}/${repo}`)}+${encodeURIComponent( + "type:issue", + )}+${encodeURIComponent("state:open")}+${encodeURIComponent(failTitle)}`, + { items: issues }, + ); + + await fail( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + errors, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true(fetch.done()); + t.true(t.context.log.calledWith("Skip commenting on or creating an issue.")); +}); \ No newline at end of file From f5162920f932260346d9fdef265e60f372c93553 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 15 Jul 2024 16:42:10 +0100 Subject: [PATCH 07/58] fix(build): lint --- test/fail.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fail.test.js b/test/fail.test.js index dd3b5610..7e18a5cd 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -525,4 +525,4 @@ test('Does not post comments on existing issues when "failCommentCondition" is " t.true(fetch.done()); t.true(t.context.log.calledWith("Skip commenting on or creating an issue.")); -}); \ No newline at end of file +}); From 460d4758602f99017185239222570a646fb5b6dd Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 16 Jul 2024 12:18:52 +0100 Subject: [PATCH 08/58] test(fail): add case for `Post new issue if none exists yet, but don't comment on existing issues when "failCommentCondition" is disallows it` --- test/fail.test.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/fail.test.js b/test/fail.test.js index 7e18a5cd..1d2bdb81 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -526,3 +526,70 @@ test('Does not post comments on existing issues when "failCommentCondition" is " t.true(fetch.done()); t.true(t.context.log.calledWith("Skip commenting on or creating an issue.")); }); + +test(`Post new issue if none exists yet, but don't comment on existing issues when "failCommentCondition" is disallows it`, async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const errors = [{ message: "An error occured" }]; + const failTitle = "The automated release is failing 🚨"; + const pluginConfig = { + failTitle, + failComment: `Error: Release for branch \${branch.name} failed with error: \${errors.map(error => error.message).join(';')}`, + failCommentCondition: "<% return !issue; %>", + }; + const options = { + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + full_name: `${owner}/${repo}`, + }) + .getOnce( + `https://api.github.local/search/issues?q=${encodeURIComponent( + "in:title", + )}+${encodeURIComponent(`repo:${owner}/${repo}`)}+${encodeURIComponent( + "type:issue", + )}+${encodeURIComponent("state:open")}+${encodeURIComponent(failTitle)}`, + { items: [] }, + ) + .postOnce( + (url, { body }) => { + t.is(url, `https://api.github.local/repos/${owner}/${repo}/issues`); + t.regex( + JSON.parse(body).body, + /Error: Release for branch master failed with error: An error occured\n\n/, + ); + return true; + }, + { html_url: "https://github.com/issues/2", number: 2 }, + ); + + await fail( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + errors, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true(fetch.done()); + t.true( + t.context.log.calledWith( + "Created issue #%d: %s.", + 2, + "https://github.com/issues/2", + ), + ); +}); From 7d2b4de551d7f446ffe936ca53e6f4700372affe Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 16 Jul 2024 14:08:03 +0100 Subject: [PATCH 09/58] test(success): add case `Does not comment on issues/PR if "successCommentCondition" is "false"` --- test/success.test.js | 64 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 5ef815ad..8d2c9dad 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2138,7 +2138,7 @@ test("Close open issues when a release is successful", async (t) => { t.true(fetch.done()); }); -test('Skip commention on issues/PR if "successComment" is "false"', async (t) => { +test('Skip comment on on issues/PR if "successComment" is "false"', async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; @@ -2198,6 +2198,66 @@ test('Skip commention on issues/PR if "successComment" is "false"', async (t) => t.true(fetch.done()); }); +test('Does not comment on issues/PR if "successCommentCondition" is "false"', 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, successCommentCondition: false }; + const options = { + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = [ + { + hash: "123", + message: "Commit 1 message\n\n Fix #1", + tree: { long: "aaa" }, + }, + ]; + 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}`, + }) + .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, + branch: { name: "master" }, + commits, + nextRelease, + releases, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true( + t.context.log.calledWith("Skip commenting on issues and pull requests."), + ); + t.true(fetch.done()); +}); + test('Skip closing issues if "failComment" is "false"', async (t) => { const owner = "test_user"; const repo = "test_repo"; @@ -2298,4 +2358,4 @@ test('Skip closing issues if "failTitle" is "false"', async (t) => { ); t.true(t.context.log.calledWith("Skip closing issue.")); t.true(fetch.done()); -}); +}); \ No newline at end of file From e55733bdb39cbc4bbc98d7387ac891d70adc8ea0 Mon Sep 17 00:00:00 2001 From: Olabode Lawal-Shittabey Date: Tue, 16 Jul 2024 19:26:27 +0100 Subject: [PATCH 10/58] Update test/fail.test.js Co-authored-by: Jonas Schubert --- test/fail.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fail.test.js b/test/fail.test.js index 1d2bdb81..3f5c078a 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -527,7 +527,7 @@ test('Does not post comments on existing issues when "failCommentCondition" is " t.true(t.context.log.calledWith("Skip commenting on or creating an issue.")); }); -test(`Post new issue if none exists yet, but don't comment on existing issues when "failCommentCondition" is disallows it`, async (t) => { +test(`Post new issue if none exists yet, but don't comment on existing issues when "failCommentCondition" disallows it`, async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; From 2156d92efa15c8a5e3b4eb33491fb55c7e9c1841 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 18 Jul 2024 22:21:44 +0100 Subject: [PATCH 11/58] test(success): add case for `Add comment and label to found issues/associatedPR using the "successCommentCondition"` --- test/success.test.js | 138 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 8d2c9dad..14c37e1b 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2198,7 +2198,7 @@ test('Skip comment on on issues/PR if "successComment" is "false"', async (t) => t.true(fetch.done()); }); -test('Does not comment on issues/PR if "successCommentCondition" is "false"', async (t) => { +test('Does not comment/label on issues/PR if "successCommentCondition" is "false"', async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; @@ -2258,6 +2258,140 @@ test('Does not comment on issues/PR if "successCommentCondition" is "false"', as t.true(fetch.done()); }); +test('Add comment and label to found issues/associatedPR using the "successCommentCondition"', 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, + // Issues with the label "semantic-release-relevant" will be commented and labeled + successCommentCondition: + "<% return issue.labels?.includes('semantic-release-relevant'); %>", + }; + const options = { + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = [ + { hash: "123", message: "Commit 1 message" }, + { hash: "456", message: "Commit 2 message" }, + ]; + const nextRelease = { version: "1.0.0" }; + const releases = [ + { name: "GitHub release", url: "https://github.com/release" }, + ]; + const issues = [ + { number: 1, body: "Issue 1 body", title: failTitle }, + { number: 2, body: `Issue 2 body\n\n${ISSUE_ID}`, title: failTitle }, + { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, + ]; + const prs = [ + { number: 4, pull_request: {}, state: "closed" }, + { + number: 5, + pull_request: {}, + state: "closed", + labels: ["semantic-release-relevant"], + }, + ]; + + 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: { + associatedPullRequests: { + nodes: [prs[0]], + }, + }, + commit456: { + associatedPullRequests: { + nodes: [prs[1]], + }, + }, + }, + }, + }) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/4/commits`, + [{ sha: commits[0].hash }], + ) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/5/commits`, + [{ sha: commits[1].hash }], + ) + .getOnce( + `https://api.github.local/search/issues?q=${encodeURIComponent( + "in:title", + )}+${encodeURIComponent(`repo:${owner}/${repo}`)}+${encodeURIComponent( + "type:issue", + )}+${encodeURIComponent("state:open")}+${encodeURIComponent(failTitle)}`, + { items: issues }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/5/comments`, + { html_url: "https://github.com/successcomment-5" }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/5/labels`, + {}, + { body: ["released"] }, + ) + .patchOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/2`, + { html_url: "https://github.com/issues/2" }, + { + body: { + state: "closed", + }, + }, + ) + .patchOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/3`, + { html_url: "https://github.com/issues/3" }, + { + body: { + state: "closed", + }, + }, + ); + + await success( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + 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", + 5, + "https://github.com/successcomment-5", + ), + ); + t.true( + t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 5), + ); + t.true(fetch.done()); +}); + test('Skip closing issues if "failComment" is "false"', async (t) => { const owner = "test_user"; const repo = "test_repo"; @@ -2358,4 +2492,4 @@ test('Skip closing issues if "failTitle" is "false"', async (t) => { ); t.true(t.context.log.calledWith("Skip closing issue.")); t.true(fetch.done()); -}); \ No newline at end of file +}); From 6f46d09fec4a3a92e724f526b3c3ab1e131ea0cd Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 18 Jul 2024 22:39:35 +0100 Subject: [PATCH 12/58] nits --- lib/success.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/success.js b/lib/success.js index 89b46e4d..7d24e987 100644 --- a/lib/success.js +++ b/lib/success.js @@ -133,11 +133,11 @@ export default async function success(pluginConfig, context, { Octokit }) { await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { - const canCommentOnIssuesAndPRs = successCommentCondition + const canCommentOnIssue = successCommentCondition ? template(successCommentCondition)({ ...context, issue }) : true; - if (canCommentOnIssuesAndPRs) { + if (canCommentOnIssue) { const body = successComment ? template(successComment)({ ...context, issue }) : getSuccessComment(issue, releaseInfos, nextRelease); From e32cc698d0168a5d8d1ddb6ad8c229ca5a2bb865 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 18 Jul 2024 22:42:18 +0100 Subject: [PATCH 13/58] test(success): add case for `Does not comment/label found associatedPR when "successCommentCondition" disables it` --- test/success.test.js | 104 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/test/success.test.js b/test/success.test.js index 14c37e1b..eddd0a88 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2392,6 +2392,110 @@ test('Add comment and label to found issues/associatedPR using the "successComme t.true(fetch.done()); }); +test('Does not comment/label found associatedPR when "successCommentCondition" disables it', 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, + successCommentCondition: "<% return !issue.pull_request; %>", + }; + const options = { + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = [ + { hash: "123", message: "Commit 1 message" }, + { hash: "456", message: "Commit 2 message" }, + ]; + const nextRelease = { version: "1.0.0" }; + const releases = [ + { name: "GitHub release", url: "https://github.com/release" }, + ]; + const issues = [ + { number: 1, body: `Issue 1 body\n\n${ISSUE_ID}`, title: failTitle }, + ]; + const prs = [ + { number: 2, pull_request: {}, state: "closed" }, + { number: 3, pull_request: {}, state: "closed" }, + ]; + + 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: { + associatedPullRequests: { + nodes: [prs[0]], + }, + }, + commit456: { + associatedPullRequests: { + nodes: [prs[1]], + }, + }, + }, + }, + }) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/2/commits`, + [{ sha: commits[0].hash }], + ) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/3/commits`, + [{ sha: commits[1].hash }], + ) + .getOnce( + `https://api.github.local/search/issues?q=${encodeURIComponent( + "in:title", + )}+${encodeURIComponent(`repo:${owner}/${repo}`)}+${encodeURIComponent( + "type:issue", + )}+${encodeURIComponent("state:open")}+${encodeURIComponent(failTitle)}`, + { items: issues }, + ) + .patchOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/1`, + { html_url: "https://github.com/issues/1" }, + { + body: { + state: "closed", + }, + }, + ); + + await success( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + commits, + nextRelease, + releases, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true( + t.context.log.calledWith( + "Closed issue #%d: %s.", + 1, + "https://github.com/issues/1", + ), + ); + t.true(fetch.done()); +}); + test('Skip closing issues if "failComment" is "false"', async (t) => { const owner = "test_user"; const repo = "test_repo"; From 81c9ff0a3077146d335e2392f1441358c2cf08ec Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 18 Jul 2024 22:58:05 +0100 Subject: [PATCH 14/58] test(success): improve case `Does not comment/label found associatedPR when "successCommentCondition" disables it` --- test/success.test.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/success.test.js b/test/success.test.js index eddd0a88..d24724c1 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2399,6 +2399,7 @@ test('Does not comment/label found associatedPR when "successCommentCondition" d const failTitle = "The automated release is failing 🚨"; const pluginConfig = { failTitle, + // Only issues will be commented and labeled (not PRs) successCommentCondition: "<% return !issue.pull_request; %>", }; const options = { @@ -2414,9 +2415,15 @@ test('Does not comment/label found associatedPR when "successCommentCondition" d ]; const issues = [ { number: 1, body: `Issue 1 body\n\n${ISSUE_ID}`, title: failTitle }, + { + number: 4, + body: `Issue 4 body`, + title: "Issue 4 title", + state: "closed", + }, ]; const prs = [ - { number: 2, pull_request: {}, state: "closed" }, + { number: 2, pull_request: {}, body: "Fixes #4", state: "closed" }, { number: 3, pull_request: {}, state: "closed" }, ]; @@ -2449,6 +2456,15 @@ test('Does not comment/label found associatedPR when "successCommentCondition" d `https://api.github.local/repos/${owner}/${repo}/pulls/3/commits`, [{ sha: commits[1].hash }], ) + .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"] }, + ) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( "in:title", @@ -2493,6 +2509,16 @@ test('Does not comment/label found associatedPR when "successCommentCondition" d "https://github.com/issues/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(fetch.done()); }); From a82c2b8b03744554b60dd0987f3fe970d802cc81 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 18 Jul 2024 23:12:12 +0100 Subject: [PATCH 15/58] doc: add documentation for `successCommentCondition` and `failCommentCondition` --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c222204e..f78cdbb9 100644 --- a/README.md +++ b/README.md @@ -80,24 +80,26 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are: ### Options -| Option | Description | Default | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `githubUrl` | The GitHub server endpoint. | `GH_URL` or `GITHUB_URL` environment variable. | -| `githubApiPathPrefix` | The GitHub API prefix, relative to `githubUrl`. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. | -| `githubApiUrl` | The GitHub API endpoint. Note that this overwrites `githubApiPathPrefix`. | `GITHUB_API_URL` environment variable. | -| `proxy` | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy). | `HTTP_PROXY` environment variable. | -| `assets` | An array of files to upload to the release. See [assets](#assets). | - | -| `successComment` | The comment to add to each issue and pull request resolved by the release. Set to `false` to disable commenting on issues and pull requests. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release]()` | -| `failComment` | The content of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. | -| `failTitle` | The title of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. | `The automated release is failing 🚨` | -| `labels` | The [labels](https://help.github.com/articles/about-labels) to add to the issue created when a release fails. Set to `false` to not add any label. | `['semantic-release']` | -| `assignees` | The [assignees](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users) to add to the issue created when a release fails. | - | -| `releasedLabels` | The [labels](https://help.github.com/articles/about-labels) to add to each issue and pull request resolved by the release. Set to `false` to not add any label. See [releasedLabels](#releasedlabels). | `['released<%= nextRelease.channel ? \` on @\${nextRelease.channel}\` : "" %>']- | -| `addReleases` | Will add release links to the GitHub Release. Can be `false`, `"bottom"` or `"top"`. See [addReleases](#addReleases). | `false` | -| `draftRelease` | A boolean indicating if a GitHub Draft Release should be created instead of publishing an actual GitHub Release. | `false` | -| `releaseNameTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's name | `<%= nextverison.name %>` | -| `releaseBodyTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's body | `<%= nextverison.notes %>` | -| `discussionCategoryName` | The category name in which to create a linked discussion to the release. Set to `false` to disable creating discussion for a release. | `false` | +| Option | Description | Default | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `githubUrl` | The GitHub server endpoint. | `GH_URL` or `GITHUB_URL` environment variable. | +| `githubApiPathPrefix` | The GitHub API prefix, relative to `githubUrl`. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. | +| `githubApiUrl` | The GitHub API endpoint. Note that this overwrites `githubApiPathPrefix`. | `GITHUB_API_URL` environment variable. | +| `proxy` | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy). | `HTTP_PROXY` environment variable. | +| `assets` | An array of files to upload to the release. See [assets](#assets). | - | +| `successComment` | The comment to add to each issue and pull request resolved by the release. Set to `false` to disable commenting on issues and pull requests. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release]()` | +| `successCommentCondition` | Use this as condition, when to comment on issues or merge requests. See [successCommentCondition](#successCommentCondition) | - | +| `failComment` | The content of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. | +| `failTitle` | The title of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. | `The automated release is failing 🚨` | +| `failCommentCondition` | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition). | - | +| `labels` | The [labels](https://help.github.com/articles/about-labels) to add to the issue created when a release fails. Set to `false` to not add any label. | `['semantic-release']` | +| `assignees` | The [assignees](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users) to add to the issue created when a release fails. | - | +| `releasedLabels` | The [labels](https://help.github.com/articles/about-labels) to add to each issue and pull request resolved by the release. Set to `false` to not add any label. See [releasedLabels](#releasedlabels). | `['released<%= nextRelease.channel ? \` on @\${nextRelease.channel}\` : "" %>']- | +| `addReleases` | Will add release links to the GitHub Release. Can be `false`, `"bottom"` or `"top"`. See [addReleases](#addReleases). | `false` | +| `draftRelease` | A boolean indicating if a GitHub Draft Release should be created instead of publishing an actual GitHub Release. | `false` | +| `releaseNameTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's name | `<%= nextverison.name %>` | +| `releaseBodyTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's body | `<%= nextverison.notes %>` | +| `discussionCategoryName` | The category name in which to create a linked discussion to the release. Set to `false` to disable creating discussion for a release. | `false` | #### proxy @@ -181,6 +183,28 @@ The `successComment` `This ${issue.pull_request ? 'pull request' : 'issue'} is i > This pull request is included in version 1.0.0 +#### successCommentCondition + +The message for the issue comments is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: + +| Parameter | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| `issue` | A [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | + +##### successCommentCondition example + +- do not create any comments at all: set to `false` or templating: `"<% return false; %>"` +- to only comment on issues: `"<% return !issue.pull_request; %>"` +- to only comment on pull requests: `"<% return issue.pull_request; %>"` +- you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant'); %>"` + +> check the [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) or the [GitHub API Issue Object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) for properties which can be used for the filter + #### failComment The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: @@ -199,6 +223,29 @@ The `failComment` `This release from branch ${branch.name} had failed due to the > - Error message 1 > - Error message 2 +#### failCommentCondition + +The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: + +| Parameter | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| `issue` | A [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | + +##### failCommentCondition example + +The `failComment` `This release from branch ${branch.name} had failed due to the following errors:\n- ${errors.map(err => err.message).join('\\n- ')}` will generate the comment: + +- do not create any comments at all: set to `false` or templating: `"<% return false; %>"` +- to only comment on main branch: `"<% return branch.name === 'main' %>"` +- you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"` + +> check the [GitHub API Issue Object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) for properties which can be used for the filter + #### releasedLabels Each label name is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: From 219386c228e55ddae282c7bc32c0b12434fc150c Mon Sep 17 00:00:00 2001 From: Olabode Lawal-Shittabey Date: Mon, 22 Jul 2024 15:41:55 +0100 Subject: [PATCH 16/58] Update lib/success.js Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- lib/success.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/success.js b/lib/success.js index 7d24e987..9b15a155 100644 --- a/lib/success.js +++ b/lib/success.js @@ -58,8 +58,7 @@ export default async function success(pluginConfig, context, { Octokit }) { if (successComment === false) { logger.log("Skip commenting on issues and pull requests."); - logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'. - Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`); + logger.warn(`DEPRECATION: 'false' for 'successComment' is deprecated and will be removed in a future major version. Use 'successCommentCondition' instead.`); } else if (successCommentCondition === false) { logger.log("Skip commenting on issues and pull requests."); } else { From 1cbf8c2c9f574d577d8211899b1bac9fcd573a88 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 22 Jul 2024 16:23:15 +0100 Subject: [PATCH 17/58] refactor: modify `failTitle`, `failComment` and `successComment` false deprecation message --- lib/fail.js | 3 +-- lib/success.js | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/fail.js b/lib/fail.js index 2e3d950e..138d69cf 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -32,8 +32,7 @@ export default async function fail(pluginConfig, context, { Octokit }) { if (failComment === false || failTitle === false) { logger.log("Skip issue creation."); - logger.error(`Failure reporting should be disabled via 'failCommentCondition'. - Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`); + logger.warn(`DEPRECATION: 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version. Use 'failCommentCondition' instead.`); } else if (failCommentCondition === false) { logger.log("Skip issue creation."); } else { diff --git a/lib/success.js b/lib/success.js index 5cc6e933..8d439c36 100644 --- a/lib/success.js +++ b/lib/success.js @@ -61,7 +61,10 @@ export default async function success(pluginConfig, context, { Octokit }) { logger.log("No commits found in release"); } logger.log("Skip commenting on issues and pull requests."); - logger.warn(`DEPRECATION: 'false' for 'successComment' is deprecated and will be removed in a future major version. Use 'successCommentCondition' instead.`); + // TODO: use logger.warn() instead of logger.log() + logger.log( + `DEPRECATION: 'false' for 'successComment' is deprecated and will be removed in a future major version. Use 'successCommentCondition' instead.`, + ); } else if (successCommentCondition === false) { logger.log("Skip commenting on issues and pull requests."); } else { From 0d3014b5cde31e344f52e5302a5ba22b8775c0a2 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 22 Jul 2024 16:25:18 +0100 Subject: [PATCH 18/58] refator: implement early return in `fail` and `success` lifecyccle helper function --- lib/fail.js | 68 ++++++++++++++++----------------- lib/success.js | 101 +++++++++++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/lib/fail.js b/lib/fail.js index 138d69cf..070b076e 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -32,7 +32,10 @@ export default async function fail(pluginConfig, context, { Octokit }) { if (failComment === false || failTitle === false) { logger.log("Skip issue creation."); - logger.warn(`DEPRECATION: 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version. Use 'failCommentCondition' instead.`); + // TODO: use logger.warn() instead of logger.log() + logger.log( + `DEPRECATION: 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version. Use 'failCommentCondition' instead.`, + ); } else if (failCommentCondition === false) { logger.log("Skip issue creation."); } else { @@ -60,41 +63,36 @@ export default async function fail(pluginConfig, context, { Octokit }) { ? template(failCommentCondition)({ ...context, issue: srIssue }) : true; - if (canCommentOnOrCreateIssue) { - if (srIssue) { - logger.log( - "Found existing semantic-release issue #%d.", - srIssue.number, - ); - const comment = { owner, repo, issue_number: srIssue.number, body }; - debug("create comment: %O", comment); - const { - data: { html_url: url }, - } = await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - comment, - ); - logger.log("Added comment to issue #%d: %s.", srIssue.number, url); - } else { - const newIssue = { - owner, - repo, - title: failTitle, - body: `${body}\n\n${ISSUE_ID}`, - labels: labels || [], - assignees, - }; - debug("create issue: %O", newIssue); - const { - data: { html_url: url, number }, - } = await octokit.request( - "POST /repos/{owner}/{repo}/issues", - newIssue, - ); - logger.log("Created issue #%d: %s.", number, url); - } - } else { + if (!canCommentOnOrCreateIssue) { logger.log("Skip commenting on or creating an issue."); + return; + } + + if (srIssue) { + logger.log("Found existing semantic-release issue #%d.", srIssue.number); + const comment = { owner, repo, issue_number: srIssue.number, body }; + debug("create comment: %O", comment); + const { + data: { html_url: url }, + } = await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + comment, + ); + logger.log("Added comment to issue #%d: %s.", srIssue.number, url); + } else { + const newIssue = { + owner, + repo, + title: failTitle, + body: `${body}\n\n${ISSUE_ID}`, + labels: labels || [], + assignees, + }; + debug("create issue: %O", newIssue); + const { + data: { html_url: url, number }, + } = await octokit.request("POST /repos/{owner}/{repo}/issues", newIssue); + logger.log("Created issue #%d: %s.", number, url); } } } diff --git a/lib/success.js b/lib/success.js index 8d439c36..9a44f8c8 100644 --- a/lib/success.js +++ b/lib/success.js @@ -142,58 +142,59 @@ export default async function success(pluginConfig, context, { Octokit }) { ? template(successCommentCondition)({ ...context, issue }) : true; - if (canCommentOnIssue) { - const body = successComment - ? template(successComment)({ ...context, issue }) - : getSuccessComment(issue, releaseInfos, nextRelease); - try { - const comment = { owner, repo, issue_number: issue.number, body }; - debug("create comment: %O", comment); - const { - data: { html_url: url }, - } = await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - comment, - ); - logger.log("Added comment to issue #%d: %s", issue.number, url); + if (!canCommentOnIssue) { + logger.log("Skip commenting on issue #%d.", issue.id); + return; + } - if (releasedLabels) { - const labels = releasedLabels.map((label) => - template(label)(context), - ); - await octokit.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/labels", - { - owner, - repo, - issue_number: issue.number, - data: labels, - }, - ); - logger.log("Added labels %O to issue #%d", labels, issue.number); - } - } catch (error) { - if (error.status === 403) { - logger.error( - "Not allowed to add a comment to the issue #%d.", - issue.number, - ); - } else if (error.status === 404) { - logger.error( - "Failed to add a comment to the issue #%d as it doesn't exist.", - issue.number, - ); - } else { - errors.push(error); - logger.error( - "Failed to add a comment to the issue #%d.", - issue.number, - ); - // Don't throw right away and continue to update other issues - } + const body = successComment + ? template(successComment)({ ...context, issue }) + : getSuccessComment(issue, releaseInfos, nextRelease); + try { + const comment = { owner, repo, issue_number: issue.number, body }; + debug("create comment: %O", comment); + const { + data: { html_url: url }, + } = await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + comment, + ); + logger.log("Added comment to issue #%d: %s", issue.number, url); + + if (releasedLabels) { + const labels = releasedLabels.map((label) => + template(label)(context), + ); + await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/labels", + { + owner, + repo, + issue_number: issue.number, + data: labels, + }, + ); + logger.log("Added labels %O to issue #%d", labels, issue.number); + } + } catch (error) { + if (error.status === 403) { + logger.error( + "Not allowed to add a comment to the issue #%d.", + issue.number, + ); + } else if (error.status === 404) { + logger.error( + "Failed to add a comment to the issue #%d as it doesn't exist.", + issue.number, + ); + } else { + errors.push(error); + logger.error( + "Failed to add a comment to the issue #%d.", + issue.number, + ); + // Don't throw right away and continue to update other issues } - } else { - logger.log("Skip commenting on issue #%d.", issue.id); } }), ); From ad0a03ff2b718ab5a898e9617a3409c51cc569aa Mon Sep 17 00:00:00 2001 From: Olabode Lawal-Shittabey Date: Wed, 24 Jul 2024 20:29:14 +0100 Subject: [PATCH 19/58] Update README.md Co-authored-by: Jonas Schubert --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f78cdbb9..20250fa7 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are: | `proxy` | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy). | `HTTP_PROXY` environment variable. | | `assets` | An array of files to upload to the release. See [assets](#assets). | - | | `successComment` | The comment to add to each issue and pull request resolved by the release. Set to `false` to disable commenting on issues and pull requests. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release]()` | -| `successCommentCondition` | Use this as condition, when to comment on issues or merge requests. See [successCommentCondition](#successCommentCondition) | - | +| `successCommentCondition` | Use this as condition, when to comment on issues or pull requests. See [successCommentCondition](#successCommentCondition) | - | | `failComment` | The content of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. | | `failTitle` | The title of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. | `The automated release is failing 🚨` | | `failCommentCondition` | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition). | - | From 1faae48223ac55cb71a677b447d09ecb3929718e Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 24 Jul 2024 20:33:04 +0100 Subject: [PATCH 20/58] remove `failCommentCondition` example wrong description --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 20250fa7..9c823338 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,6 @@ The message for the issue content is generated with [Lodash template](https://lo ##### failCommentCondition example -The `failComment` `This release from branch ${branch.name} had failed due to the following errors:\n- ${errors.map(err => err.message).join('\\n- ')}` will generate the comment: - - do not create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on main branch: `"<% return branch.name === 'main' %>"` - you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"` From 924ca5543bcbd13d94a8df053d9eef8991f6073e Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 24 Jul 2024 20:38:45 +0100 Subject: [PATCH 21/58] build: fix lint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c823338..3d89b8a9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are: | `proxy` | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy). | `HTTP_PROXY` environment variable. | | `assets` | An array of files to upload to the release. See [assets](#assets). | - | | `successComment` | The comment to add to each issue and pull request resolved by the release. Set to `false` to disable commenting on issues and pull requests. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release]()` | -| `successCommentCondition` | Use this as condition, when to comment on issues or pull requests. See [successCommentCondition](#successCommentCondition) | - | +| `successCommentCondition` | Use this as condition, when to comment on issues or pull requests. See [successCommentCondition](#successCommentCondition) | - | | `failComment` | The content of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. | | `failTitle` | The title of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. | `The automated release is failing 🚨` | | `failCommentCondition` | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition). | - | From cfc61eb91ba0e183875c738583591439d6fff21d Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 5 Aug 2024 22:04:51 +0100 Subject: [PATCH 22/58] feat: add validators for `successCommentCondition` and `failCommentCondition` --- lib/verify.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/verify.js b/lib/verify.js index b951cee3..1f93d9d8 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -38,8 +38,10 @@ const VALIDATORS = { (isPlainObject(asset) && isStringOrStringArray(asset.path)), ), successComment: canBeDisabled(isNonEmptyString), + successCommentCondition: canBeDisabled(isNonEmptyString), failTitle: canBeDisabled(isNonEmptyString), failComment: canBeDisabled(isNonEmptyString), + failCommentCondition: canBeDisabled(isNonEmptyString), labels: canBeDisabled(isArrayOf(isNonEmptyString)), assignees: isArrayOf(isNonEmptyString), releasedLabels: canBeDisabled(isArrayOf(isNonEmptyString)), From 9591bd00b6d7c574ae2331101ac8ac3370c7c46c Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 6 Aug 2024 16:55:28 +0100 Subject: [PATCH 23/58] feat: add `buildAssociatedPRs` to create pr object in form of issue object with pull_request property --- lib/success.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/success.js b/lib/success.js index 9a44f8c8..70c651c8 100644 --- a/lib/success.js +++ b/lib/success.js @@ -79,9 +79,7 @@ export default async function success(pluginConfig, context, { Octokit }) { buildAssociatedPRsQuery(shas), { owner, repo }, ); - const associatedPRs = Object.values(repository).map( - (item) => item.associatedPullRequests.nodes, - ); + const associatedPRs = buildAssociatedPRs(repository); const uniqueAssociatedPRs = uniqBy(flatten(associatedPRs), "number"); @@ -292,3 +290,25 @@ export function buildAssociatedPRsQuery(shas) { } `; } + +/** + * Build associatedPRs (to issue-like object with `pull_request` property) from the GraphQL repository response + * @param {object} repository + * @returns {object[]} + */ +function buildAssociatedPRs(repository) { + const associatedPRs = []; + for (const commit in repository) { + for (const node of repository[commit].associatedPullRequests.nodes) { + const pr = { + number: node.number, + body: node.body, + pull_request: { + url: node.url + } + }; + associatedPRs.push(pr); + } + } + return associatedPRs; +} \ No newline at end of file From 1cbb41ab9858e2777bbe76eb21fb8e5638b81ce7 Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 6 Aug 2024 17:17:50 +0100 Subject: [PATCH 24/58] doc: update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d89b8a9..20264a54 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ The message for the issue comments is generated with [Lodash template](https://l | `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | | `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | | `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | -| `issue` | A [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | +| `issue` | A [GitHub API Pull Request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | ##### successCommentCondition example @@ -203,7 +203,7 @@ The message for the issue comments is generated with [Lodash template](https://l - to only comment on pull requests: `"<% return issue.pull_request; %>"` - you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant'); %>"` -> check the [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) or the [GitHub API Issue Object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) for properties which can be used for the filter +> check the [GitHub API pull request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for properties which can be used for the filter #### failComment @@ -234,7 +234,7 @@ The message for the issue content is generated with [Lodash template](https://lo | `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | | `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | | `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | -| `issue` | A [GitHub API pull request object](https://developer.github.com/v3/search/#search-issues) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | +| `issue` | A [GitHub API pull request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | ##### failCommentCondition example @@ -242,7 +242,7 @@ The message for the issue content is generated with [Lodash template](https://lo - to only comment on main branch: `"<% return branch.name === 'main' %>"` - you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"` -> check the [GitHub API Issue Object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) for properties which can be used for the filter +> check the [GitHub API Pull Request Object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for properties which can be used for the filter #### releasedLabels From 8cfa35024dfef6b96e8deef30dd64ded3241c075 Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 6 Aug 2024 17:25:53 +0100 Subject: [PATCH 25/58] build: fix lint --- README.md | 28 ++++++++++++++-------------- lib/success.js | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 20264a54..bb60253d 100644 --- a/README.md +++ b/README.md @@ -187,13 +187,13 @@ The `successComment` `This ${issue.pull_request ? 'pull request' : 'issue'} is i The message for the issue comments is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: -| Parameter | Description | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | -| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | -| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | -| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | -| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | | `issue` | A [GitHub API Pull Request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | ##### successCommentCondition example @@ -227,13 +227,13 @@ The `failComment` `This release from branch ${branch.name} had failed due to the The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: -| Parameter | Description | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | -| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | -| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | -| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | -| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | | `issue` | A [GitHub API pull request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for pull requests related to a commit, or an `Object` with the `number` property for issues resolved via [keywords](https://help.github.com/articles/closing-issues-using-keywords) | ##### failCommentCondition example diff --git a/lib/success.js b/lib/success.js index 70c651c8..c4ce5104 100644 --- a/lib/success.js +++ b/lib/success.js @@ -304,11 +304,11 @@ function buildAssociatedPRs(repository) { number: node.number, body: node.body, pull_request: { - url: node.url - } + url: node.url, + }, }; associatedPRs.push(pr); } } return associatedPRs; -} \ No newline at end of file +} From 6ee6c24504cbe8acdf16a22df8ed414cae4266ec Mon Sep 17 00:00:00 2001 From: babblebey Date: Tue, 6 Aug 2024 17:44:39 +0100 Subject: [PATCH 26/58] build: fix failing test `Add custom comment and labels` --- test/success.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 4b45f9f7..14c48ae1 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -1172,11 +1172,9 @@ test("Add custom comment and labels", async (t) => { ) .postOnce( `https://api.github.local/repos/${owner}/${repo}/issues/1/comments`, - { html_url: "https://github.com/successcomment-1" }, { - body: { - body: `last release: ${lastRelease.version} nextRelease: ${nextRelease.version} branch: master commits: 1 releases: 1 PR attribute: PR prop`, - }, + html_url: "https://github.com/successcomment-1", + body: `last release: ${lastRelease.version} nextRelease: ${nextRelease.version} branch: master commits: 1 releases: 1 PR attribute: PR prop`, }, ) .postOnce( From f250e71202d5a351fc9912a8d23b563c7fcf0d74 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 8 Aug 2024 00:57:51 +0100 Subject: [PATCH 27/58] feat: request more field for `associatedPRs` via graphql and improve `buildAssociatedPRs` response object with --- lib/success.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/lib/success.js b/lib/success.js index c4ce5104..3c099334 100644 --- a/lib/success.js +++ b/lib/success.js @@ -277,9 +277,62 @@ export function buildAssociatedPRsQuery(shas) { ...on Commit { associatedPullRequests(first: 100) { nodes { + id + title + body url number - body + createdAt + updatedAt + closedAt + mergedAt + isDraft + mergedBy { + login + avatarUrl + url + } + commits { + totalCount + } + comments { + totalCount + } + state + author { + login + url + avatarUrl + } + labels(first: 100) { + nodes { + id + url + name + color + } + } + milestone { + url + id + number + state + title + description + creator { + login + url + avatarUrl + } + createdAt + closedAt + updatedAt + } + locked + activeLockReason + mergeable + canBeRebased + changedFiles } } } @@ -292,7 +345,7 @@ export function buildAssociatedPRsQuery(shas) { } /** - * Build associatedPRs (to issue-like object with `pull_request` property) from the GraphQL repository response + * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response * @param {object} repository * @returns {object[]} */ @@ -301,11 +354,53 @@ function buildAssociatedPRs(repository) { for (const commit in repository) { for (const node of repository[commit].associatedPullRequests.nodes) { const pr = { + pull_request: true, number: node.number, + title: node.title, body: node.body, - pull_request: { - url: node.url, + labels: node.labels?.nodes.map((label) => label.name), + html_url: node.url, + created_at: node.createdAt, + updated_at: node.updatedAt, + closed_at: node.closedAt, + merged_at: node.mergedAt, + draft: node.isDraft, + user: { + login: node.author?.login, + html_url: node.author?.url, + avatar_url: node.author?.avatarUrl, + }, + commits: node.commits?.totalCount, + comments: node.comments?.totalCount, + state: node.state, + merged_by: { + login: node.mergedBy?.login, + avatar_url: node.mergedBy?.avatarUrl, + html_url: node.mergedBy?.url, }, + milestone: node.milestone + ? { + url: node.milestone.url, + id: node.milestone.id, + number: node.milestone.number, + state: node.milestone.state, + title: node.milestone.title, + description: node.milestone.description, + creator: { + login: node.milestone.creator.login, + html_url: node.milestone.creator.url, + avatar_url: node.milestone.creator.avatarUrl, + }, + created_at: node.milestone.createdAt, + closed_at: node.milestone.closedAt, + updated_at: node.milestone.updatedAt, + } + : null, + locked: node.locked, + active_lock_reason: node.activeLockReason, + mergeable: node.mergeable, + rebaseable: node.canBeRebased, + changed_files: node.changedFiles, }; associatedPRs.push(pr); } From 60a73d62b75b3c9ae73719f21ad1a2e7f2b0cf51 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 8 Aug 2024 00:58:08 +0100 Subject: [PATCH 28/58] test: modify integration tests --- test/integration.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration.test.js b/test/integration.test.js index b9f2f299..6c11f562 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -409,7 +409,7 @@ test("Comment and add labels on PR included in the releases", async (t) => { const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -615,7 +615,7 @@ test("Verify, release and notify success", async (t) => { const uploadOrigin = "https://github.com"; const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`; const uploadUrl = `${uploadOrigin}${uploadUri}{?name,label}`; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const commits = [{ hash: "123", message: "Commit 1 message" }]; const fetch = fetchMock @@ -773,7 +773,7 @@ test("Verify, update release and notify success", async (t) => { }; const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`; const releaseId = 1; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const commits = [ { hash: "123", message: "Commit 1 message", tree: { long: "aaa" } }, ]; From 4b689fef29fffd84bb5843110b51a9d7f463de17 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 8 Aug 2024 00:58:27 +0100 Subject: [PATCH 29/58] test: modify `success` unit tests --- test/success.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 14c48ae1..96627ff6 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2339,12 +2339,21 @@ test('Add comment and label to found issues/associatedPR using the "successComme { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, ]; const prs = [ - { number: 4, pull_request: {}, state: "closed" }, + { number: 4, pull_request: true, state: "closed" }, { number: 5, - pull_request: {}, + pull_request: true, state: "closed", - labels: ["semantic-release-relevant"], + labels: { + nodes: [ + { + id: 123, + color: "000000", + name: "semantic-release-relevant", + url: `https://github.com/${owner}/${repo}/labels/semantic-release-relevant`, + }, + ], + }, }, ]; From 627b0d82c0531221bac4b24e8475c222335f25d6 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 19:57:41 +0100 Subject: [PATCH 30/58] build: fix lint --- lib/success.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index aff5c566..94d5aeda 100644 --- a/lib/success.js +++ b/lib/success.js @@ -444,4 +444,4 @@ function buildAssociatedPRs(repository) { } } return associatedPRs; -} \ No newline at end of file +} From 863b4a65e260379cdb2e4f7dbdd2b02067a91dab Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 21:06:24 +0100 Subject: [PATCH 31/58] build: fix failing tests --- test/success.test.js | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index aae7af76..f7bf170b 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2685,7 +2685,7 @@ test('Add comment and label to found issues/associatedPR using the "successComme failTitle, // Issues with the label "semantic-release-relevant" will be commented and labeled successCommentCondition: - "<% return issue.labels?.includes('semantic-release-relevant'); %>", + "<% return issue.labels.includes('semantic-release-relevant'); %>", }; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, @@ -2704,24 +2704,22 @@ test('Add comment and label to found issues/associatedPR using the "successComme { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, ]; const prs = [ - { number: 4, pull_request: true, state: "closed" }, + { + number: 4, + pull_request: true, + state: "closed", + labels: [], + }, { number: 5, pull_request: true, state: "closed", - labels: { - nodes: [ - { - id: 123, - color: "000000", - name: "semantic-release-relevant", - url: `https://github.com/${owner}/${repo}/labels/semantic-release-relevant`, - }, - ], - }, + labels: ["semantic-release-relevant"], }, ]; + // t.log(prs); + const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { @@ -2731,12 +2729,22 @@ test('Add comment and label to found issues/associatedPR using the "successComme 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]], }, }, @@ -2863,12 +2871,22 @@ test('Does not comment/label found associatedPR when "successCommentCondition" d 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]], }, }, From 2d826fa7269243433b6ce16bd476421bff69f1cc Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 21:07:00 +0100 Subject: [PATCH 32/58] feat: add `__typename` to `issue.user` object --- lib/success.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/success.js b/lib/success.js index 94d5aeda..d425448a 100644 --- a/lib/success.js +++ b/lib/success.js @@ -341,6 +341,7 @@ function buildAssociatedPRsQuery(shas) { login url avatarUrl + __typename } labels(first: 100) { nodes { @@ -407,6 +408,7 @@ function buildAssociatedPRs(repository) { login: node.author?.login, html_url: node.author?.url, avatar_url: node.author?.avatarUrl, + type: node.author?.__typename, }, commits: node.commits?.totalCount, comments: node.comments?.totalCount, From 990ed772cb699adb7a78147d59182d519d7dd617 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 21:32:45 +0100 Subject: [PATCH 33/58] test: add new case `Does not comment/label associatedPR created by "Bots"` --- test/success.test.js | 175 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 2 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index f7bf170b..f482578c 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2676,7 +2676,7 @@ test('Does not comment/label on issues/PR if "successCommentCondition" is "false t.true(fetch.done()); }); -test('Add comment and label to found issues/associatedPR using the "successCommentCondition"', async (t) => { +test('Add comment and label to found issues/associatedPR using the "successCommentCondition": if specific label is found', async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; @@ -2827,7 +2827,178 @@ test('Add comment and label to found issues/associatedPR using the "successComme t.true(fetch.done()); }); -test('Does not comment/label found associatedPR when "successCommentCondition" disables it', async (t) => { +test('Does not comment/label associatedPR created by "Bots"', 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, + // Only issues will be commented and labeled (not PRs) + successCommentCondition: + "<% return !issue.user || issue.user.type !== 'Bot'; %>", + }; + const options = { + repositoryUrl: `https://github.com/${owner}/${repo}.git`, + }; + const commits = [ + { hash: "123", message: "Commit 1 message" }, + { hash: "456", message: "Commit 2 message" }, + ]; + const nextRelease = { version: "1.0.0" }; + const releases = [ + { name: "GitHub release", url: "https://github.com/release" }, + ]; + const issues = [ + { number: 1, body: `Issue 1 body\n\n${ISSUE_ID}`, title: failTitle }, + { + number: 4, + body: `Issue 4 body`, + title: "Issue 4 title", + state: "closed", + }, + ]; + const prs = [ + { + number: 2, + pull_request: {}, + body: "Fixes #4", + state: "closed", + user: { + login: "user_login", + type: "User", + avatar_url: "https://some_url.link", + html_url: "https://some_url.link", + }, + }, + { + number: 3, + pull_request: {}, + state: "closed", + user: { + login: "bot_user_login", + type: "Bot", + avatar_url: "https://some_url.link", + html_url: "https://some_url.link", + }, + }, + ]; + + 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: "NI", + hasNextPage: false, + }, + nodes: [prs[0]], + }, + }, + commit456: { + oid: "456", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[1]], + }, + }, + }, + }, + }) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/2/commits`, + [{ sha: commits[0].hash }], + ) + .getOnce( + `https://api.github.local/repos/${owner}/${repo}/pulls/3/commits`, + [{ sha: commits[1].hash }], + ) + .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/2/comments`, + { html_url: "https://github.com/successcomment-2" }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/2/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: issues }, + ) + .patchOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/1`, + { html_url: "https://github.com/issues/1" }, + { + body: { + state: "closed", + }, + }, + ); + + await success( + pluginConfig, + { + env, + options, + branch: { name: "master" }, + commits, + nextRelease, + releases, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.true( + t.context.log.calledWith( + "Closed issue #%d: %s.", + 1, + "https://github.com/issues/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(fetch.done()); +}); + +test('Does not comment/label some associatedPR when "successCommentCondition" disables it: Don\'t comment on PRs created by BOTs', async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; From 9e19862266c99e354760e70f6c6bdf55b611c553 Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 21:33:41 +0100 Subject: [PATCH 34/58] test: modify `pull_request` mock value to `boolean` --- test/success.test.js | 68 ++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index f482578c..45ffd277 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -28,8 +28,8 @@ test("Add comment and labels to PRs associated with release commits and issues s 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: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, body: "Fixes #3", state: "closed" }, ]; const options = { branch: "master", @@ -210,10 +210,10 @@ test("Add comment and labels to PRs associated with release commits and issues ( 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" }, + { number: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, body: "Fixes #3", state: "closed" }, + { number: 5, pull_request: true, state: "closed" }, + { number: 6, pull_request: true, state: "closed" }, ]; const options = { branch: "master", @@ -405,8 +405,8 @@ test("Add comment and labels to PRs associated with release commits and issues c 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: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, body: "Fixes #3", state: "closed" }, ]; const options = { branch: "master", @@ -601,12 +601,12 @@ test("Make multiple search queries if necessary", async (t) => { const failTitle = "The automated release is failing 🚨"; const pluginConfig = { failTitle }; const prs = [ - { number: 1, pull_request: {}, state: "closed" }, - { number: 2, pull_request: {}, state: "closed" }, - { number: 3, pull_request: {}, state: "closed" }, - { number: 4, pull_request: {}, state: "closed" }, - { number: 5, pull_request: {}, state: "closed" }, - { number: 6, pull_request: {}, state: "closed" }, + { number: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, state: "closed" }, + { number: 3, pull_request: true, state: "closed" }, + { number: 4, pull_request: true, state: "closed" }, + { number: 5, pull_request: true, state: "closed" }, + { number: 6, pull_request: true, state: "closed" }, ]; const options = { branch: "master", @@ -885,8 +885,8 @@ test("Do not add comment and labels for unrelated PR returned by search (compare const failTitle = "The automated release is failing 🚨"; const pluginConfig = { failTitle }; const prs = [ - { number: 1, pull_request: {}, state: "closed" }, - { number: 2, pull_request: {}, state: "closed" }, + { number: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, state: "closed" }, ]; const options = { branch: "master", @@ -1240,9 +1240,9 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { const failTitle = "The automated release is failing 🚨"; const pluginConfig = { failTitle }; const prs = [ - { number: 1, pull_request: {}, state: "closed" }, - { number: 2, pull_request: {}, body: "Fixes #4", state: "closed" }, - { number: 3, pull_request: {}, body: "Fixes #5", state: "closed" }, + { number: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, body: "Fixes #4", state: "closed" }, + { number: 3, pull_request: true, body: "Fixes #5", state: "closed" }, ]; const options = { branch: "master", @@ -1433,7 +1433,7 @@ test("Add custom comment and labels", async (t) => { ], }; const prs = [ - { number: 1, prop: "PR prop", pull_request: {}, state: "closed" }, + { number: 1, prop: "PR prop", pull_request: true, state: "closed" }, ]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; const lastRelease = { version: "1.0.0" }; @@ -1533,7 +1533,7 @@ test("Add custom label", async (t) => { const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: ["custom label"], failTitle }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; const lastRelease = { version: "1.0.0" }; const commits = [{ hash: "123", message: "Commit 1 message" }]; @@ -1629,7 +1629,7 @@ test("Comment on issue/PR without ading a label", async (t) => { const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, failTitle }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; const lastRelease = { version: "1.0.0" }; const commits = [{ hash: "123", message: "Commit 1 message" }]; @@ -1713,7 +1713,7 @@ test("Editing the release to include all release links at the bottom", async (t) const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "bottom" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -1823,7 +1823,7 @@ test("Editing the release to include all release links at the top", async (t) => const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "top" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -1933,7 +1933,7 @@ test("Editing the release to include all release links with no additional releas const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "top" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -2029,7 +2029,7 @@ test("Editing the release to include all release links with no additional releas const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "bottom" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -2125,7 +2125,7 @@ test("Editing the release to include all release links with no releases", async const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "bottom" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, }; @@ -2214,7 +2214,7 @@ test("Editing the release with no ID in the release", async (t) => { const env = { GITHUB_TOKEN: "github_token" }; const failTitle = "The automated release is failing 🚨"; const pluginConfig = { releasedLabels: false, addReleases: "bottom" }; - const prs = [{ number: 1, pull_request: {}, state: "closed" }]; + const prs = [{ number: 1, pull_request: true, state: "closed" }]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; const nextRelease = { version: "2.0.0", @@ -2311,8 +2311,8 @@ test("Ignore errors when adding comments and closing issues", async (t) => { { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, ]; const prs = [ - { number: 1, pull_request: {}, state: "closed" }, - { number: 2, pull_request: {}, state: "closed" }, + { number: 1, pull_request: true, state: "closed" }, + { number: 2, pull_request: true, state: "closed" }, ]; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, @@ -2861,7 +2861,7 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { const prs = [ { number: 2, - pull_request: {}, + pull_request: true, body: "Fixes #4", state: "closed", user: { @@ -2873,7 +2873,7 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { }, { number: 3, - pull_request: {}, + pull_request: true, state: "closed", user: { login: "bot_user_login", @@ -3029,8 +3029,8 @@ test('Does not comment/label some associatedPR when "successCommentCondition" di }, ]; const prs = [ - { number: 2, pull_request: {}, body: "Fixes #4", state: "closed" }, - { number: 3, pull_request: {}, state: "closed" }, + { number: 2, pull_request: true, body: "Fixes #4", state: "closed" }, + { number: 3, pull_request: true, state: "closed" }, ]; const fetch = fetchMock From a052c4982130c1748b1d253233026ea586bdee1b Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 15 Aug 2024 21:34:46 +0100 Subject: [PATCH 35/58] chore(test): clean debug comments --- test/success.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 45ffd277..9ff694f9 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -2718,8 +2718,6 @@ test('Add comment and label to found issues/associatedPR using the "successComme }, ]; - // t.log(prs); - const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { From 54dd0368aa850c68a1ed4a4878f7b95f102d8419 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 17:25:02 +0100 Subject: [PATCH 36/58] feat: re-integrate `buildAssociatedPRs` --- lib/success.js | 116 ++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/lib/success.js b/lib/success.js index d425448a..bb565d55 100644 --- a/lib/success.js +++ b/lib/success.js @@ -93,7 +93,7 @@ export default async function success(pluginConfig, context, { Octokit }) { (item) => item.associatedPullRequests, ); for (const { nodes, pageInfo } of responseAssociatedPRs) { - associatedPRs.push(nodes); + associatedPRs.push(...buildAssociatedPRs(nodes)); if (pageInfo.hasNextPage) { let cursor = pageInfo.endCursor; let hasNextPage = true; @@ -103,7 +103,9 @@ export default async function success(pluginConfig, context, { Octokit }) { { owner, repo, sha: response.commit.oid, cursor }, ); const { associatedPullRequests } = repository.commit; - associatedPRs.push(associatedPullRequests.nodes); + associatedPRs.push( + ...buildAssociatedPRs(associatedPullRequests.nodes), + ); if (associatedPullRequests.pageInfo.hasNextPage) { cursor = associatedPullRequests.pageInfo.endCursor; } else { @@ -385,65 +387,63 @@ function buildAssociatedPRsQuery(shas) { /** * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response - * @param {object} repository + * @param {object} associatedPRsResponseNodes * @returns {object[]} */ -function buildAssociatedPRs(repository) { +function buildAssociatedPRs(associatedPRsResponseNodes) { const associatedPRs = []; - for (const commit in repository) { - for (const node of repository[commit].associatedPullRequests.nodes) { - const pr = { - pull_request: true, - number: node.number, - title: node.title, - body: node.body, - labels: node.labels?.nodes.map((label) => label.name), - html_url: node.url, - created_at: node.createdAt, - updated_at: node.updatedAt, - closed_at: node.closedAt, - merged_at: node.mergedAt, - draft: node.isDraft, - user: { - login: node.author?.login, - html_url: node.author?.url, - avatar_url: node.author?.avatarUrl, - type: node.author?.__typename, - }, - commits: node.commits?.totalCount, - comments: node.comments?.totalCount, - state: node.state, - merged_by: { - login: node.mergedBy?.login, - avatar_url: node.mergedBy?.avatarUrl, - html_url: node.mergedBy?.url, - }, - milestone: node.milestone - ? { - url: node.milestone.url, - id: node.milestone.id, - number: node.milestone.number, - state: node.milestone.state, - title: node.milestone.title, - description: node.milestone.description, - creator: { - login: node.milestone.creator.login, - html_url: node.milestone.creator.url, - avatar_url: node.milestone.creator.avatarUrl, - }, - created_at: node.milestone.createdAt, - closed_at: node.milestone.closedAt, - updated_at: node.milestone.updatedAt, - } - : null, - locked: node.locked, - active_lock_reason: node.activeLockReason, - mergeable: node.mergeable, - rebaseable: node.canBeRebased, - changed_files: node.changedFiles, - }; - associatedPRs.push(pr); - } + for (const node of associatedPRsResponseNodes) { + const pr = { + pull_request: true, + number: node.number, + title: node.title, + body: node.body, + labels: node.labels?.nodes.map((label) => label.name), + html_url: node.url, + created_at: node.createdAt, + updated_at: node.updatedAt, + closed_at: node.closedAt, + merged_at: node.mergedAt, + draft: node.isDraft, + user: { + login: node.author?.login, + html_url: node.author?.url, + avatar_url: node.author?.avatarUrl, + type: node.author?.__typename, + }, + commits: node.commits?.totalCount, + comments: node.comments?.totalCount, + state: node.state, + merged_by: { + login: node.mergedBy?.login, + avatar_url: node.mergedBy?.avatarUrl, + html_url: node.mergedBy?.url, + }, + milestone: node.milestone + ? { + url: node.milestone.url, + id: node.milestone.id, + number: node.milestone.number, + state: node.milestone.state, + title: node.milestone.title, + description: node.milestone.description, + creator: { + login: node.milestone.creator.login, + html_url: node.milestone.creator.url, + avatar_url: node.milestone.creator.avatarUrl, + }, + created_at: node.milestone.createdAt, + closed_at: node.milestone.closedAt, + updated_at: node.milestone.updatedAt, + } + : null, + locked: node.locked, + active_lock_reason: node.activeLockReason, + mergeable: node.mergeable, + rebaseable: node.canBeRebased, + changed_files: node.changedFiles, + }; + associatedPRs.push(pr); } return associatedPRs; } From a6c84423ef08101b22f79dae733ee9317ce4bdbb Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 17:36:18 +0100 Subject: [PATCH 37/58] feat: re-introduced and modifed `loadSingleCommitAssociatedPRs` --- lib/success.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/lib/success.js b/lib/success.js index bb565d55..21afc73b 100644 --- a/lib/success.js +++ b/lib/success.js @@ -385,6 +385,86 @@ 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 { + oid + associatedPullRequests(after: $cursor, first: 100) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + title + body + url + number + createdAt + updatedAt + closedAt + mergedAt + isDraft + mergedBy { + login + avatarUrl + url + } + commits { + totalCount + } + comments { + totalCount + } + state + author { + login + url + avatarUrl + __typename + } + labels(first: 100) { + nodes { + id + url + name + color + } + } + milestone { + url + id + number + state + title + description + creator { + login + url + avatarUrl + } + createdAt + closedAt + updatedAt + } + locked + activeLockReason + mergeable + canBeRebased + changedFiles + } + } + } + } + } + } +`; + /** * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response * @param {object} associatedPRsResponseNodes From e0e7ff14995b4ab9494a612c7a80cb59ca0360e1 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 17:47:22 +0100 Subject: [PATCH 38/58] refactor: introduce `parsedIssues` as returned value from `prs**.body` and `commits**.message` --- lib/success.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/success.js b/lib/success.js index 21afc73b..19c3c815 100644 --- a/lib/success.js +++ b/lib/success.js @@ -147,7 +147,7 @@ export default async function success(pluginConfig, context, { Octokit }) { ); // Parse the release commits message and PRs body to find resolved issues/PRs via comment keyworkds - const issues = [ + const parsedIssues = [ ...prs.map((pr) => pr.body), ...commits.map((commit) => commit.message), ].reduce( @@ -167,7 +167,7 @@ export default async function success(pluginConfig, context, { Octokit }) { [], ); - debug("found issues via comments: %O", issues); + debug("found issues via comments: %O", parsedIssues); await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { From c068bb32cc0cd17221750a11e2e58bf6bbacf725 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 17:55:41 +0100 Subject: [PATCH 39/58] feat: added `buildRelatedIssuesQuery` util graphql query builder --- lib/success.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/lib/success.js b/lib/success.js index 19c3c815..97aed5fa 100644 --- a/lib/success.js +++ b/lib/success.js @@ -297,6 +297,69 @@ export default async function success(pluginConfig, context, { Octokit }) { } } +/** + * Builds GraphQL query for fetching PRs/Commits related Issues to a list of commit hash (sha) + * @param {Array} numbers + * @returns {string} + */ +function buildRelatedIssuesQuery(numbers) { + return `#graphql + query getRelatedIssues($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + ${numbers + .map((num) => { + return `issue${num}: issue(number: "${num}") { + id + title + body + url + number + createdAt + updatedAt + closedAt + comments { + totalCount + } + state + author { + login + url + avatarUrl + __typename + } + labels(first: 100) { + nodes { + id + url + name + color + } + } + milestone { + url + id + number + state + title + description + creator { + login + url + avatarUrl + } + createdAt + closedAt + updatedAt + } + locked + }` + }).join("") + } + } + } + `; +} + /** * Builds GraphQL query for fetching associated PRs to a list of commit hash (sha) * @param {Array} shas From 63a4bc18350dcf9d3f1a3f21407a8a65aaf85c95 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 18:06:35 +0100 Subject: [PATCH 40/58] feat: implement computation for `responseRelatedIssues` --- lib/success.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/success.js b/lib/success.js index 97aed5fa..88b24a1c 100644 --- a/lib/success.js +++ b/lib/success.js @@ -169,6 +169,17 @@ export default async function success(pluginConfig, context, { Octokit }) { debug("found issues via comments: %O", parsedIssues); + const { repository } = await octokit.graphql( + buildRelatedIssuesQuery( + uniqBy(flatten(parsedIssues), "number").map((issue) => issue.number), + ), + { owner, repo }, + ); + + const responseRelatedIssues = Object.values(repository).map( + (issue) => issue, + ); + await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { const canCommentOnIssue = successCommentCondition @@ -352,9 +363,9 @@ function buildRelatedIssuesQuery(numbers) { updatedAt } locked - }` - }).join("") - } + }`; + }) + .join("")} } } `; From 229d4a8d9cc493dd5efebd0e0108696aef602121 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 19:53:47 +0100 Subject: [PATCH 41/58] fix: correct `number` arg type in `buildRelatedIssuesQuery` --- lib/success.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index 88b24a1c..0d38ee4f 100644 --- a/lib/success.js +++ b/lib/success.js @@ -319,7 +319,7 @@ function buildRelatedIssuesQuery(numbers) { repository(owner: $owner, name: $repo) { ${numbers .map((num) => { - return `issue${num}: issue(number: "${num}") { + return `issue${num}: issue(number: ${num}) { id title body @@ -338,6 +338,7 @@ function buildRelatedIssuesQuery(numbers) { avatarUrl __typename } + authorAssociation labels(first: 100) { nodes { id From 43ca24facd4877408ba2855dc9d56909446dcf97 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 20:14:30 +0100 Subject: [PATCH 42/58] refactor: extract common field accross graphql queries to `baseFields` --- lib/success.js | 198 +++++++++++++++---------------------------------- 1 file changed, 60 insertions(+), 138 deletions(-) diff --git a/lib/success.js b/lib/success.js index 0d38ee4f..53eab4e7 100644 --- a/lib/success.js +++ b/lib/success.js @@ -308,6 +308,57 @@ export default async function success(pluginConfig, context, { Octokit }) { } } +/** + * Fields common accross PRs and Issue + */ +const baseFields = ` + id + title + body + url + number + createdAt + updatedAt + closedAt + comments { + totalCount + } + state + author { + login + url + avatarUrl + __typename + } + authorAssociation + activeLockReason + labels(first: 100) { + nodes { + id + url + name + color + } + } + milestone { + url + id + number + state + title + description + creator { + login + url + avatarUrl + } + createdAt + closedAt + updatedAt + } + locked +`; + /** * Builds GraphQL query for fetching PRs/Commits related Issues to a list of commit hash (sha) * @param {Array} numbers @@ -320,50 +371,7 @@ function buildRelatedIssuesQuery(numbers) { ${numbers .map((num) => { return `issue${num}: issue(number: ${num}) { - id - title - body - url - number - createdAt - updatedAt - closedAt - comments { - totalCount - } - state - author { - login - url - avatarUrl - __typename - } - authorAssociation - labels(first: 100) { - nodes { - id - url - name - color - } - } - milestone { - url - id - number - state - title - description - creator { - login - url - avatarUrl - } - createdAt - closedAt - updatedAt - } - locked + ${baseFields} }`; }) .join("")} @@ -392,14 +400,10 @@ function buildAssociatedPRsQuery(shas) { hasNextPage } nodes { - id - title - body - url - number - createdAt - updatedAt - closedAt + ${baseFields} + mergeable + canBeRebased + changedFiles mergedAt isDraft mergedBy { @@ -410,45 +414,6 @@ function buildAssociatedPRsQuery(shas) { commits { totalCount } - comments { - totalCount - } - state - author { - login - url - avatarUrl - __typename - } - labels(first: 100) { - nodes { - id - url - name - color - } - } - milestone { - url - id - number - state - title - description - creator { - login - url - avatarUrl - } - createdAt - closedAt - updatedAt - } - locked - activeLockReason - mergeable - canBeRebased - changedFiles } } } @@ -475,14 +440,10 @@ const loadSingleCommitAssociatedPRs = `#graphql hasNextPage } nodes { - id - title - body - url - number - createdAt - updatedAt - closedAt + ${baseFields} + mergeable + canBeRebased + changedFiles mergedAt isDraft mergedBy { @@ -493,45 +454,6 @@ const loadSingleCommitAssociatedPRs = `#graphql commits { totalCount } - comments { - totalCount - } - state - author { - login - url - avatarUrl - __typename - } - labels(first: 100) { - nodes { - id - url - name - color - } - } - milestone { - url - id - number - state - title - description - creator { - login - url - avatarUrl - } - createdAt - closedAt - updatedAt - } - locked - activeLockReason - mergeable - canBeRebased - changedFiles } } } From c4c561d56dcf7eb63905c8d70e4e7c2c129d7666 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 21:05:15 +0100 Subject: [PATCH 43/58] refactor: transform `buildAssociatedPRs` to `buildIssuesOrPRsFromResponseNode` with ability to build both `PRs` and `Issues` object --- lib/success.js | 52 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/success.js b/lib/success.js index 53eab4e7..2370ade7 100644 --- a/lib/success.js +++ b/lib/success.js @@ -1,4 +1,4 @@ -import { isNil, uniqBy, template, flatten, isEmpty } from "lodash-es"; +import { isNil, uniqBy, template, flatten, isEmpty, merge } from "lodash-es"; import pFilter from "p-filter"; import AggregateError from "aggregate-error"; import issueParser from "issue-parser"; @@ -464,14 +464,14 @@ const loadSingleCommitAssociatedPRs = `#graphql /** * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response - * @param {object} associatedPRsResponseNodes + * @param {object} responseNodes + * @param {"ISSUE" | "PR"}} type * @returns {object[]} */ -function buildAssociatedPRs(associatedPRsResponseNodes) { - const associatedPRs = []; - for (const node of associatedPRsResponseNodes) { - const pr = { - pull_request: true, +function buildIssuesOrPRsFromResponseNode(responseNodes, type) { + const resultArray = []; + for (const node of responseNodes) { + let baseProps = { number: node.number, title: node.title, body: node.body, @@ -479,23 +479,14 @@ function buildAssociatedPRs(associatedPRsResponseNodes) { html_url: node.url, created_at: node.createdAt, updated_at: node.updatedAt, - closed_at: node.closedAt, - merged_at: node.mergedAt, - draft: node.isDraft, user: { login: node.author?.login, html_url: node.author?.url, avatar_url: node.author?.avatarUrl, type: node.author?.__typename, }, - commits: node.commits?.totalCount, comments: node.comments?.totalCount, state: node.state, - merged_by: { - login: node.mergedBy?.login, - avatar_url: node.mergedBy?.avatarUrl, - html_url: node.mergedBy?.url, - }, milestone: node.milestone ? { url: node.milestone.url, @@ -516,11 +507,30 @@ function buildAssociatedPRs(associatedPRsResponseNodes) { : null, locked: node.locked, active_lock_reason: node.activeLockReason, - mergeable: node.mergeable, - rebaseable: node.canBeRebased, - changed_files: node.changedFiles, + closed_at: node.closedAt, }; - associatedPRs.push(pr); + + let result = baseProps; + + if (type === "PR") { + const prProps = { + pull_request: true, + mergeable: node.mergeable, + rebaseable: node.canBeRebased, + changed_files: node.changedFiles, + commits: node.commits?.totalCount, + merged_at: node.mergedAt, + draft: node.isDraft, + merged_by: { + login: node.mergedBy?.login, + avatar_url: node.mergedBy?.avatarUrl, + html_url: node.mergedBy?.url, + }, + } + result = merge(baseProps, prProps); + } + + resultArray.push(result); } - return associatedPRs; + return resultArray; } From 88a71188f6ee3664631cebffcfc59eff631f62b3 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 21:23:15 +0100 Subject: [PATCH 44/58] feat: integrate `buildIssuesOrPRsFromResponseNode` --- lib/success.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/success.js b/lib/success.js index 2370ade7..038245d6 100644 --- a/lib/success.js +++ b/lib/success.js @@ -93,7 +93,7 @@ export default async function success(pluginConfig, context, { Octokit }) { (item) => item.associatedPullRequests, ); for (const { nodes, pageInfo } of responseAssociatedPRs) { - associatedPRs.push(...buildAssociatedPRs(nodes)); + associatedPRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); if (pageInfo.hasNextPage) { let cursor = pageInfo.endCursor; let hasNextPage = true; @@ -104,7 +104,7 @@ export default async function success(pluginConfig, context, { Octokit }) { ); const { associatedPullRequests } = repository.commit; associatedPRs.push( - ...buildAssociatedPRs(associatedPullRequests.nodes), + ...buildIssuesOrPRsFromResponseNode(associatedPullRequests.nodes, "PR"), ); if (associatedPullRequests.pageInfo.hasNextPage) { cursor = associatedPullRequests.pageInfo.endCursor; @@ -167,18 +167,18 @@ export default async function success(pluginConfig, context, { Octokit }) { [], ); - debug("found issues via comments: %O", parsedIssues); - const { repository } = await octokit.graphql( buildRelatedIssuesQuery( uniqBy(flatten(parsedIssues), "number").map((issue) => issue.number), ), { owner, repo }, ); - const responseRelatedIssues = Object.values(repository).map( (issue) => issue, ); + const issues = buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + + debug("found related issues via PRs and Commits: %O", issues.length); await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { @@ -465,7 +465,7 @@ const loadSingleCommitAssociatedPRs = `#graphql /** * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response * @param {object} responseNodes - * @param {"ISSUE" | "PR"}} type + * @param {"ISSUE" | "PR"} type * @returns {object[]} */ function buildIssuesOrPRsFromResponseNode(responseNodes, type) { From 1bc293c5f306ffdb53000b46a5c8fb17cd6b0c93 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 21:26:31 +0100 Subject: [PATCH 45/58] feat: implement `issueOrPR` for correctly addressing issues and pr in logs --- lib/success.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/success.js b/lib/success.js index 038245d6..5cd67361 100644 --- a/lib/success.js +++ b/lib/success.js @@ -178,7 +178,7 @@ export default async function success(pluginConfig, context, { Octokit }) { ); const issues = buildIssuesOrPRsFromResponseNode(responseRelatedIssues); - debug("found related issues via PRs and Commits: %O", issues.length); + debug("found related issues via PRs and Commits: %O", issues); await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { @@ -196,6 +196,7 @@ export default async function success(pluginConfig, context, { Octokit }) { : getSuccessComment(issue, releaseInfos, nextRelease); try { const comment = { owner, repo, issue_number: issue.number, body }; + const issueOrPR = issue.pull_request ? "PR" : "issue"; debug("create comment: %O", comment); const { data: { html_url: url }, @@ -203,7 +204,7 @@ export default async function success(pluginConfig, context, { Octokit }) { "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", comment, ); - logger.log("Added comment to issue #%d: %s", issue.number, url); + logger.log(`Added comment to ${issueOrPR} #%d: %s`, issue.number, url); if (releasedLabels) { const labels = releasedLabels.map((label) => @@ -218,23 +219,23 @@ export default async function success(pluginConfig, context, { Octokit }) { data: labels, }, ); - logger.log("Added labels %O to issue #%d", labels, issue.number); + logger.log(`Added labels %O to ${issueOrPR} #%d`, labels, issue.number); } } catch (error) { if (error.status === 403) { logger.error( - "Not allowed to add a comment to the issue #%d.", + `Not allowed to add a comment to the ${issueOrPR} #%d.`, issue.number, ); } else if (error.status === 404) { logger.error( - "Failed to add a comment to the issue #%d as it doesn't exist.", + `Failed to add a comment to the ${issueOrPR} #%d as it doesn't exist.`, issue.number, ); } else { errors.push(error); logger.error( - "Failed to add a comment to the issue #%d.", + `Failed to add a comment to the ${issueOrPR} #%d.`, issue.number, ); // Don't throw right away and continue to update other issues From e95a700ce774edd51cbcb1f363c520389739f35b Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 22:38:22 +0100 Subject: [PATCH 46/58] feat: implement improved chunk operation helper `inChunks` and integrate in pr and issues fetch --- lib/success.js | 65 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/success.js b/lib/success.js index 5cd67361..c4741a32 100644 --- a/lib/success.js +++ b/lib/success.js @@ -75,16 +75,9 @@ export default async function success(pluginConfig, context, { Octokit }) { const releaseInfos = releases.filter((release) => Boolean(release.name)); const shas = commits.map(({ hash }) => hash); - const associatedPRs = []; - - // 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) { + // Get associatedPRs + const [ associatedPRs ] = await Promise.all([inChunks(shas, 100, async (chunk) => { + const responsePRs = []; const { repository } = await octokit.graphql( buildAssociatedPRsQuery(chunk), { owner, repo }, @@ -93,7 +86,7 @@ export default async function success(pluginConfig, context, { Octokit }) { (item) => item.associatedPullRequests, ); for (const { nodes, pageInfo } of responseAssociatedPRs) { - associatedPRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); + responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); if (pageInfo.hasNextPage) { let cursor = pageInfo.endCursor; let hasNextPage = true; @@ -103,7 +96,7 @@ export default async function success(pluginConfig, context, { Octokit }) { { owner, repo, sha: response.commit.oid, cursor }, ); const { associatedPullRequests } = repository.commit; - associatedPRs.push( + responsePRs.push( ...buildIssuesOrPRsFromResponseNode(associatedPullRequests.nodes, "PR"), ); if (associatedPullRequests.pageInfo.hasNextPage) { @@ -114,7 +107,8 @@ export default async function success(pluginConfig, context, { Octokit }) { } } } - } + return responsePRs; + })]); const uniqueAssociatedPRs = uniqBy(flatten(associatedPRs), "number"); @@ -167,16 +161,19 @@ export default async function success(pluginConfig, context, { Octokit }) { [], ); - const { repository } = await octokit.graphql( - buildRelatedIssuesQuery( - uniqBy(flatten(parsedIssues), "number").map((issue) => issue.number), - ), - { owner, repo }, - ); - const responseRelatedIssues = Object.values(repository).map( - (issue) => issue, - ); - const issues = buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + const uniqueParsedIssues = uniqBy(flatten(parsedIssues), "number"); + + // Get relatedIssues + const [ issues ] = await Promise.all([inChunks(uniqueParsedIssues, 100, async (chunk) => { + const { repository } = await octokit.graphql( + buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), + { owner, repo }, + ); + const responseRelatedIssues = Object.values(repository).map( + (issue) => issue, + ); + return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + })]); debug("found related issues via PRs and Commits: %O", issues); @@ -309,6 +306,28 @@ 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 + * results again. + * + * @template TItem + * @template TCallbackResult + * @param {TItem[]} items + * @param {number} chunkSize + * @param {(items: TItem[]) => TCallbackResult} callback + * @returns TCallbackResult + */ +async function inChunks(items, chunkSize, callback) { + const chunkCalls = []; + for (let i = 0; i < items.length; i += chunkSize) { + chunkCalls.push(callback(items.slice(i, i + chunkSize))); + } + const results = await Promise.all(chunkCalls); + + return results.flat(); +} + /** * Fields common accross PRs and Issue */ From 2572defcbc8ba796ab2b5e457546ce100aa34915 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 19 Aug 2024 22:39:12 +0100 Subject: [PATCH 47/58] build: fix lints --- lib/success.js | 105 ++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/lib/success.js b/lib/success.js index c4741a32..12d053e0 100644 --- a/lib/success.js +++ b/lib/success.js @@ -75,40 +75,45 @@ export default async function success(pluginConfig, context, { Octokit }) { const releaseInfos = releases.filter((release) => Boolean(release.name)); const shas = commits.map(({ hash }) => hash); - // Get associatedPRs - const [ associatedPRs ] = await Promise.all([inChunks(shas, 100, async (chunk) => { - const responsePRs = []; - const { repository } = await octokit.graphql( - buildAssociatedPRsQuery(chunk), - { owner, repo }, - ); - const responseAssociatedPRs = Object.values(repository).map( - (item) => item.associatedPullRequests, - ); - for (const { nodes, pageInfo } of responseAssociatedPRs) { - responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); - 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; - responsePRs.push( - ...buildIssuesOrPRsFromResponseNode(associatedPullRequests.nodes, "PR"), - ); - if (associatedPullRequests.pageInfo.hasNextPage) { - cursor = associatedPullRequests.pageInfo.endCursor; - } else { - hasNextPage = false; + // Get associatedPRs + const [associatedPRs] = await Promise.all([ + inChunks(shas, 100, async (chunk) => { + const responsePRs = []; + const { repository } = await octokit.graphql( + buildAssociatedPRsQuery(chunk), + { owner, repo }, + ); + const responseAssociatedPRs = Object.values(repository).map( + (item) => item.associatedPullRequests, + ); + for (const { nodes, pageInfo } of responseAssociatedPRs) { + responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); + 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; + responsePRs.push( + ...buildIssuesOrPRsFromResponseNode( + associatedPullRequests.nodes, + "PR", + ), + ); + if (associatedPullRequests.pageInfo.hasNextPage) { + cursor = associatedPullRequests.pageInfo.endCursor; + } else { + hasNextPage = false; + } } } } - } - return responsePRs; - })]); + return responsePRs; + }), + ]); const uniqueAssociatedPRs = uniqBy(flatten(associatedPRs), "number"); @@ -164,16 +169,18 @@ export default async function success(pluginConfig, context, { Octokit }) { const uniqueParsedIssues = uniqBy(flatten(parsedIssues), "number"); // Get relatedIssues - const [ issues ] = await Promise.all([inChunks(uniqueParsedIssues, 100, async (chunk) => { - const { repository } = await octokit.graphql( - buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), - { owner, repo }, - ); - const responseRelatedIssues = Object.values(repository).map( - (issue) => issue, - ); - return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); - })]); + const [issues] = await Promise.all([ + inChunks(uniqueParsedIssues, 100, async (chunk) => { + const { repository } = await octokit.graphql( + buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), + { owner, repo }, + ); + const responseRelatedIssues = Object.values(repository).map( + (issue) => issue, + ); + return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + }), + ]); debug("found related issues via PRs and Commits: %O", issues); @@ -201,7 +208,11 @@ export default async function success(pluginConfig, context, { Octokit }) { "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", comment, ); - logger.log(`Added comment to ${issueOrPR} #%d: %s`, issue.number, url); + logger.log( + `Added comment to ${issueOrPR} #%d: %s`, + issue.number, + url, + ); if (releasedLabels) { const labels = releasedLabels.map((label) => @@ -216,7 +227,11 @@ export default async function success(pluginConfig, context, { Octokit }) { data: labels, }, ); - logger.log(`Added labels %O to ${issueOrPR} #%d`, labels, issue.number); + logger.log( + `Added labels %O to ${issueOrPR} #%d`, + labels, + issue.number, + ); } } catch (error) { if (error.status === 403) { @@ -485,7 +500,7 @@ const loadSingleCommitAssociatedPRs = `#graphql /** * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response * @param {object} responseNodes - * @param {"ISSUE" | "PR"} type + * @param {"ISSUE" | "PR"} type * @returns {object[]} */ function buildIssuesOrPRsFromResponseNode(responseNodes, type) { @@ -546,7 +561,7 @@ function buildIssuesOrPRsFromResponseNode(responseNodes, type) { avatar_url: node.mergedBy?.avatarUrl, html_url: node.mergedBy?.url, }, - } + }; result = merge(baseProps, prProps); } From d41a1dfcd21da33e3cec64ac346a7f5b7199d660 Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 21 Aug 2024 18:26:07 +0100 Subject: [PATCH 48/58] test: update `integrations` test --- test/integration.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration.test.js b/test/integration.test.js index cc4b01ed..f512c7fa 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -522,13 +522,13 @@ test("Comment and add labels on PR included in the releases", async (t) => { t.deepEqual(t.context.log.args[0], ["Verify GitHub authentication"]); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 1), ); t.true(fetch.done()); }); From 6473eb915cf0ed5be59733c5341cdc60b308bee7 Mon Sep 17 00:00:00 2001 From: babblebey Date: Wed, 21 Aug 2024 18:39:54 +0100 Subject: [PATCH 49/58] test: address PR and Issue naming in logs in `success` --- test/success.test.js | 90 +++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 9ff694f9..2df9ec17 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -162,23 +162,23 @@ test("Add comment and labels to PRs associated with release commits and issues s t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 1), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 2, "https://github.com/successcomment-2", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 2), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 2), ); t.true( t.context.log.calledWith( @@ -363,14 +363,12 @@ test("Add comment and labels to PRs associated with release commits and issues ( t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%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 labels %O toPR #%d", ["released"], 1)); t.true( t.context.log.calledWith( "Added comment to issue #%d: %s", @@ -383,13 +381,13 @@ test("Add comment and labels to PRs associated with release commits and issues ( ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 6, "https://github.com/successcomment-6", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 6), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 6), ); t.true(fetch.done()); }); @@ -537,28 +535,28 @@ test("Add comment and labels to PRs associated with release commits and issues c t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://custom-url.com/successcomment-1", ), ); t.true( t.context.log.calledWith( - "Added labels %O to issue #%d", + "Added labels %O to PR #%d", ["released on @next"], 1, ), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 2, "https://custom-url.com/successcomment-2", ), ); t.true( t.context.log.calledWith( - "Added labels %O to issue #%d", + "Added labels %O to PR #%d", ["released on @next"], 2, ), @@ -817,63 +815,63 @@ test("Make multiple search queries if necessary", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 1), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 2, "https://github.com/successcomment-2", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 2), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 2), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 3, "https://github.com/successcomment-3", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 3), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 3), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 4, "https://github.com/successcomment-4", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 4), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 4), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 5, "https://github.com/successcomment-5", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 5), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 5), ); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 6, "https://github.com/successcomment-6", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 6), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 6), ); t.true(fetch.done()); }); @@ -987,13 +985,13 @@ test("Do not add comment and labels for unrelated PR returned by search (compare t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 1), ); t.true(fetch.done()); }); @@ -1376,13 +1374,13 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 1), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 1), ); t.true( t.context.log.calledWith( @@ -1512,14 +1510,14 @@ test("Add custom comment and labels", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( t.context.log.calledWith( - "Added labels %O to issue #%d", + "Added labels %O to PR #%d", ["released on @next", "released from master"], 1, ), @@ -1608,17 +1606,13 @@ test("Add custom label", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), ); t.true( - t.context.log.calledWith( - "Added labels %O to issue #%d", - ["custom label"], - 1, - ), + t.context.log.calledWith("Added labels %O to PR #%d", ["custom label"], 1), ); t.true(fetch.done()); }); @@ -1699,7 +1693,7 @@ test("Comment on issue/PR without ading a label", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -1809,7 +1803,7 @@ test("Editing the release to include all release links at the bottom", async (t) t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -1919,7 +1913,7 @@ test("Editing the release to include all release links at the top", async (t) => t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -2015,7 +2009,7 @@ test("Editing the release to include all release links with no additional releas t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -2111,7 +2105,7 @@ test("Editing the release to include all release links with no additional releas t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -2200,7 +2194,7 @@ test("Editing the release to include all release links with no releases", async t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -2291,7 +2285,7 @@ test("Editing the release with no ID in the release", async (t) => { t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 1, "https://github.com/successcomment-1", ), @@ -2437,7 +2431,7 @@ test("Ignore errors when adding comments and closing issues", async (t) => { t.true(t.context.error.calledWith("Failed to close the issue #%d.", 2)); t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 2, "https://github.com/successcomment-2", ), @@ -2814,13 +2808,13 @@ test('Add comment and label to found issues/associatedPR using the "successComme t.true( t.context.log.calledWith( - "Added comment to issue #%d: %s", + "Added comment to PR #%d: %s", 5, "https://github.com/successcomment-5", ), ); t.true( - t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 5), + t.context.log.calledWith("Added labels %O to PR #%d", ["released"], 5), ); t.true(fetch.done()); }); From 94f397599a0f6f910e6719131ea3f2b13e8a2eaa Mon Sep 17 00:00:00 2001 From: babblebey Date: Thu, 22 Aug 2024 16:09:47 +0100 Subject: [PATCH 50/58] refactor: why the `Promise.all()`? Removed it haha --- lib/success.js | 95 +++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/lib/success.js b/lib/success.js index 12d053e0..ca8b9268 100644 --- a/lib/success.js +++ b/lib/success.js @@ -76,44 +76,42 @@ export default async function success(pluginConfig, context, { Octokit }) { const shas = commits.map(({ hash }) => hash); // Get associatedPRs - const [associatedPRs] = await Promise.all([ - inChunks(shas, 100, async (chunk) => { - const responsePRs = []; - const { repository } = await octokit.graphql( - buildAssociatedPRsQuery(chunk), - { owner, repo }, - ); - const responseAssociatedPRs = Object.values(repository).map( - (item) => item.associatedPullRequests, - ); - for (const { nodes, pageInfo } of responseAssociatedPRs) { - responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); - 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; - responsePRs.push( - ...buildIssuesOrPRsFromResponseNode( - associatedPullRequests.nodes, - "PR", - ), - ); - if (associatedPullRequests.pageInfo.hasNextPage) { - cursor = associatedPullRequests.pageInfo.endCursor; - } else { - hasNextPage = false; - } + const associatedPRs = await inChunks(shas, 100, async (chunk) => { + const responsePRs = []; + const { repository } = await octokit.graphql( + buildAssociatedPRsQuery(chunk), + { owner, repo }, + ); + const responseAssociatedPRs = Object.values(repository).map( + (item) => item.associatedPullRequests, + ); + for (const { nodes, pageInfo } of responseAssociatedPRs) { + responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); + 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; + responsePRs.push( + ...buildIssuesOrPRsFromResponseNode( + associatedPullRequests.nodes, + "PR", + ), + ); + if (associatedPullRequests.pageInfo.hasNextPage) { + cursor = associatedPullRequests.pageInfo.endCursor; + } else { + hasNextPage = false; } } } - return responsePRs; - }), - ]); + } + return responsePRs; + }); const uniqueAssociatedPRs = uniqBy(flatten(associatedPRs), "number"); @@ -169,20 +167,21 @@ export default async function success(pluginConfig, context, { Octokit }) { const uniqueParsedIssues = uniqBy(flatten(parsedIssues), "number"); // Get relatedIssues - const [issues] = await Promise.all([ - inChunks(uniqueParsedIssues, 100, async (chunk) => { - const { repository } = await octokit.graphql( - buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), - { owner, repo }, - ); - const responseRelatedIssues = Object.values(repository).map( - (issue) => issue, - ); - return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); - }), - ]); + const issues = await inChunks(uniqueParsedIssues, 100, async (chunk) => { + const { repository } = await octokit.graphql( + buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), + { owner, repo }, + ); + const responseRelatedIssues = Object.values(repository).map( + (issue) => issue, + ); + return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + }); - debug("found related issues via PRs and Commits: %O", issues); + debug( + "found related issues via PRs and Commits: %O", + issues.map((issue) => issue.number), + ); await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { From e29b3123021b55fb6ba64bb030913cdfd762c93e Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 26 Aug 2024 17:22:34 +0100 Subject: [PATCH 51/58] feat: set default `type` param in `buildIssuesOrPRsFromResponseNode` --- lib/success.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index ca8b9268..56f523f2 100644 --- a/lib/success.js +++ b/lib/success.js @@ -502,7 +502,7 @@ const loadSingleCommitAssociatedPRs = `#graphql * @param {"ISSUE" | "PR"} type * @returns {object[]} */ -function buildIssuesOrPRsFromResponseNode(responseNodes, type) { +function buildIssuesOrPRsFromResponseNode(responseNodes, type = "ISSUE") { const resultArray = []; for (const node of responseNodes) { let baseProps = { From 474a28a59d3e6842eb3e731ed58fb8989d36f0a3 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 26 Aug 2024 21:37:49 +0100 Subject: [PATCH 52/58] feat: address edge cases --- lib/success.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/success.js b/lib/success.js index 56f523f2..13287fb2 100644 --- a/lib/success.js +++ b/lib/success.js @@ -86,6 +86,8 @@ export default async function success(pluginConfig, context, { Octokit }) { (item) => item.associatedPullRequests, ); for (const { nodes, pageInfo } of responseAssociatedPRs) { + if (nodes.length === 0) continue; + responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); if (pageInfo.hasNextPage) { let cursor = pageInfo.endCursor; @@ -164,24 +166,28 @@ export default async function success(pluginConfig, context, { Octokit }) { [], ); - const uniqueParsedIssues = uniqBy(flatten(parsedIssues), "number"); + let issues = []; - // Get relatedIssues - const issues = await inChunks(uniqueParsedIssues, 100, async (chunk) => { - const { repository } = await octokit.graphql( - buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), - { owner, repo }, - ); - const responseRelatedIssues = Object.values(repository).map( - (issue) => issue, - ); - return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); - }); + if (!isEmpty(parsedIssues)) { + const uniqueParsedIssues = uniqBy(flatten(parsedIssues), "number"); - debug( - "found related issues via PRs and Commits: %O", - issues.map((issue) => issue.number), - ); + // Get relatedIssues + issues = await inChunks(uniqueParsedIssues, 100, async (chunk) => { + const { repository } = await octokit.graphql( + buildRelatedIssuesQuery(chunk.map((issue) => issue.number)), + { owner, repo }, + ); + const responseRelatedIssues = Object.values(repository).map( + (issue) => issue, + ); + return buildIssuesOrPRsFromResponseNode(responseRelatedIssues); + }); + + debug( + "found related issues via PRs and Commits: %O", + issues.map((issue) => issue.number), + ); + } await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { From 8ee367933274518e9470643bb825798278f149ca Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 30 Aug 2024 21:00:49 +0100 Subject: [PATCH 53/58] test: fixed matchers in graphql request in `success` units --- test/success.test.js | 940 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 792 insertions(+), 148 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index 2df9ec17..aaebc32f 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -59,32 +59,116 @@ test("Add comment and labels to PRs associated with release commits and issues s full_name: `${redirectedOwner}/${redirectedRepo}`, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) - .postOnce("https://api.github.local/graphql", { - data: { - repository: { - commit123: { - oid: "123", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + 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]], }, - nodes: [prs[0]], }, }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + }, + }, + ) + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue3: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/3", + number: 3, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, }, - nodes: [prs[1]], + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/3", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, }, }, }, }, - }) + ) .getOnce( `https://api.github.local/repos/${redirectedOwner}/${redirectedRepo}/pulls/1/commits`, [{ sha: commits[0].hash }], @@ -299,18 +383,95 @@ test("Add comment and labels to PRs associated with release commits and issues ( overwriteRoutes: true, }, ) + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue3: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/3", + number: 3, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/4", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + }, + }, + }, + ) .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", - }, + `https://api.github.local/repos/${owner}/${repo}/issues/3/comments`, + { html_url: "https://github.com/successcomment-3" }, ) .postOnce( - `https://api.github.local/repos/${owner}/${repo}/issues/1/labels`, + `https://api.github.local/repos/${owner}/${repo}/issues/3/labels`, {}, { body: ["released"] }, ) @@ -363,12 +524,14 @@ test("Add comment and labels to PRs associated with release commits and issues ( t.true( t.context.log.calledWith( - "Added comment to PR #%d: %s", - 1, - "https://github.com/successcomment-1", + "Added comment to issue #%d: %s", + 3, + "https://github.com/successcomment-3", ), ); - t.true(t.context.log.calledWith("Added labels %O toPR #%d", ["released"], 1)); + t.true( + t.context.log.calledWith("Added labels %O to issue #%d", ["released"], 3), + ); t.true( t.context.log.calledWith( "Added comment to issue #%d: %s", @@ -428,32 +591,116 @@ test("Add comment and labels to PRs associated with release commits and issues c .getOnce(`https://custom-url.com/prefix/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, }) - .postOnce("https://custom-url.com/prefix/graphql", { - data: { - repository: { - commit123: { - oid: "123", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://custom-url.com/prefix/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + 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]], }, - nodes: [prs[0]], }, }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + }, + }, + ) + .postOnce( + (url, { body }) => + url === "https://custom-url.com/prefix/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue3: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://custom-url.com/owner/repo/issues/3", + number: 3, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, }, - nodes: [prs[1]], + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://custom-url.com/owner/repo/issues/4", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, }, }, }, }, - }) + ) .getOnce( `https://custom-url.com/prefix/repos/${owner}/${repo}/pulls/1/commits`, [{ sha: commits[0].hash }], @@ -1182,6 +1429,53 @@ test("Do not add comment and labels to PR/issues from other repo", async (t) => }, }, }) + .postOnce( + (url, { body }) => { + t.is(url, "https://api.github.local/graphql"); + t.regex(JSON.parse(body).query, /query getRelatedIssues\(/); + return true; + }, + { + data: { + repository: { + issue2: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/14", + number: 2, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + }, + }, + }, + ) .postOnce( `https://api.github.local/repos/${owner}/${repo}/issues/2/comments`, { html_url: "https://github.com/successcomment-2" }, @@ -1262,42 +1556,160 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { full_name: `${owner}/${repo}`, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) - .postOnce("https://api.github.local/graphql", { - data: { - repository: { - commit123: { - oid: "123", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + data: { + repository: { + commit123: { + oid: "123", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[0]], }, - nodes: [prs[0]], }, - }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + commit456: { + oid: "456", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[1]], + }, + }, + commit789: { + oid: "789", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[2]], }, - nodes: [prs[1]], }, }, - commit789: { - oid: "789", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + }, + }, + ) + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/4", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, }, - nodes: [prs[2]], + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue5: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/5", + number: 5, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue1: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/1", + number: 1, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, }, }, }, }, - }) + ) .getOnce( `https://api.github.local/repos/${owner}/${repo}/pulls/1/commits`, [{ sha: commits[0].hash }], @@ -1404,13 +1816,13 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { ); t.true( t.context.error.calledWith( - "Failed to add a comment to the issue #%d as it doesn't exist.", + "Failed to add a comment to the issue/PR #%d as it doesn't exist.", 2, ), ); t.true( t.context.error.calledWith( - "Not allowed to add a comment to the issue #%d.", + "Not allowed to add a comment to the issue/PR #%d.", 3, ), ); @@ -2426,7 +2838,10 @@ test("Ignore errors when adding comments and closing issues", async (t) => { t.is(error1.status, 400); t.is(error2.status, 500); t.true( - t.context.error.calledWith("Failed to add a comment to the issue #%d.", 1), + t.context.error.calledWith( + "Failed to add a comment to the issue/PR #%d.", + 1, + ), ); t.true(t.context.error.calledWith("Failed to close the issue #%d.", 2)); t.true( @@ -2697,18 +3112,105 @@ test('Add comment and label to found issues/associatedPR using the "successComme { number: 2, body: `Issue 2 body\n\n${ISSUE_ID}`, title: failTitle }, { number: 3, body: `Issue 3 body\n\n${ISSUE_ID}`, title: failTitle }, ]; + const prs = [ { - number: 4, - pull_request: true, - state: "closed", - labels: [], + id: "PR_kwDOMLlZj85z_R2M", + title: "fix: will semantic-release recognize the associated issue ", + body: "", + url: "https://github.com/babblebey/sr-github/pull/12", + number: 5, + createdAt: "2024-06-30T14:43:48Z", + updatedAt: "2024-08-26T16:19:57Z", + closedAt: "2024-06-30T14:44:05Z", + comments: { + totalCount: 12, + }, + state: "MERGED", + author: { + login: "babblebey", + url: "https://github.com/babblebey", + avatarUrl: + "https://avatars.githubusercontent.com/u/25631971?u=f4597764b2c31478a516d97bb9ecd019b5e62ae7&v=4", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "LA_kwDOMLlZj88AAAABp9kjcQ", + url: "https://github.com/babblebey/sr-github/labels/released", + name: "semantic-release-relevant", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + mergeable: "UNKNOWN", + canBeRebased: false, + changedFiles: 1, + mergedAt: "2024-06-30T14:44:05Z", + isDraft: false, + mergedBy: { + login: "babblebey", + avatarUrl: + "https://avatars.githubusercontent.com/u/25631971?u=f4597764b2c31478a516d97bb9ecd019b5e62ae7&v=4", + url: "https://github.com/babblebey", + }, + commits: { + totalCount: 1, + }, }, { - number: 5, - pull_request: true, - state: "closed", - labels: ["semantic-release-relevant"], + id: "PR_kwDOMLlZj85z_R2M", + title: "fix: will semantic-release recognize the associated issue ", + body: "", + url: "https://github.com/babblebey/sr-github/pull/12", + number: 4, + createdAt: "2024-06-30T14:43:48Z", + updatedAt: "2024-08-26T16:19:57Z", + closedAt: "2024-06-30T14:44:05Z", + comments: { + totalCount: 12, + }, + state: "MERGED", + author: { + login: "babblebey", + url: "https://github.com/babblebey", + avatarUrl: + "https://avatars.githubusercontent.com/u/25631971?u=f4597764b2c31478a516d97bb9ecd019b5e62ae7&v=4", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "LA_kwDOMLlZj88AAAABp9kjcQ", + url: "https://github.com/babblebey/sr-github/labels/released", + name: "released", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + mergeable: "UNKNOWN", + canBeRebased: false, + changedFiles: 1, + mergedAt: "2024-06-30T14:44:05Z", + isDraft: false, + mergedBy: { + login: "babblebey", + avatarUrl: + "https://avatars.githubusercontent.com/u/25631971?u=f4597764b2c31478a516d97bb9ecd019b5e62ae7&v=4", + url: "https://github.com/babblebey", + }, + commits: { + totalCount: 1, + }, }, ]; @@ -2717,32 +3219,37 @@ test('Add comment and label to found issues/associatedPR using the "successComme .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: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + data: { + repository: { + commit123: { + oid: "123", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[0]], }, - nodes: [prs[0]], }, - }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + commit456: { + oid: "456", + associatedPullRequests: { + pageInfo: { + endCursor: "NI", + hasNextPage: false, + }, + nodes: [prs[1]], }, - nodes: [prs[1]], }, }, }, }, - }) + ) .getOnce( `https://api.github.local/repos/${owner}/${repo}/pulls/4/commits`, [{ sha: commits[0].hash }], @@ -2819,16 +3326,15 @@ test('Add comment and label to found issues/associatedPR using the "successComme t.true(fetch.done()); }); -test('Does not comment/label associatedPR created by "Bots"', async (t) => { +test('Does not comment/label associatedPR and relatedIssues created by "Bots"', 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, - // Only issues will be commented and labeled (not PRs) - successCommentCondition: - "<% return !issue.user || issue.user.type !== 'Bot'; %>", + // Only issues or PRs not created by "Bot" will be commented and labeled + successCommentCondition: "<% return issue.user.type !== 'Bot'; %>", }; const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git`, @@ -2843,12 +3349,6 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { ]; const issues = [ { number: 1, body: `Issue 1 body\n\n${ISSUE_ID}`, title: failTitle }, - { - number: 4, - body: `Issue 4 body`, - title: "Issue 4 title", - state: "closed", - }, ]; const prs = [ { @@ -2866,6 +3366,7 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { { number: 3, pull_request: true, + body: "Fixes #5", state: "closed", user: { login: "bot_user_login", @@ -2881,32 +3382,116 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { .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: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + 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]], }, - nodes: [prs[0]], }, }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + }, + }, + ) + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/4", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, }, - nodes: [prs[1]], + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + }, + issue5: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/5", + number: 5, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, + }, + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "Bot", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, }, }, }, }, - }) + ) .getOnce( `https://api.github.local/repos/${owner}/${repo}/pulls/2/commits`, [{ sha: commits[0].hash }], @@ -2933,6 +3518,15 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { {}, { body: ["released"] }, ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/3/comments`, + { html_url: "https://github.com/successcomment-3" }, + ) + .postOnce( + `https://api.github.local/repos/${owner}/${repo}/issues/3/labels`, + {}, + { body: ["released"] }, + ) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( "in:title", @@ -2990,7 +3584,7 @@ test('Does not comment/label associatedPR created by "Bots"', async (t) => { t.true(fetch.done()); }); -test('Does not comment/label some associatedPR when "successCommentCondition" disables it: Don\'t comment on PRs created by BOTs', async (t) => { +test('Does not comment/label "associatedPR" when "successCommentCondition" disables it: Only comment on "relatedIssues"', async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GITHUB_TOKEN: "github_token" }; @@ -3030,32 +3624,82 @@ test('Does not comment/label some associatedPR when "successCommentCondition" di .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: "NI", - hasNextPage: false, + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getAssociatedPRs("), + { + 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]], }, - nodes: [prs[0]], }, }, - commit456: { - oid: "456", - associatedPullRequests: { - pageInfo: { - endCursor: "NI", - hasNextPage: false, + }, + }, + ) + .postOnce( + (url, { body }) => + url === "https://api.github.local/graphql" && + JSON.parse(body).query.includes("query getRelatedIssues("), + { + data: { + repository: { + issue4: { + id: "I_kw", + title: "issue title", + body: "", + url: "https://github.com/owner/repo/issues/4", + number: 4, + createdAt: "2024-07-13T09:58:09Z", + updatedAt: "2024-08-26T16:19:59Z", + closedAt: "2024-07-13T09:58:51Z", + comments: { + totalCount: 12, }, - nodes: [prs[1]], + state: "CLOSED", + author: { + login: "user", + url: "author_url", + avatarUrl: "author_avatar_url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, }, }, }, }, - }) + ) .getOnce( `https://api.github.local/repos/${owner}/${repo}/pulls/2/commits`, [{ sha: commits[0].hash }], From f617843674cae9070b9a43145325b8a0327ae83a Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 30 Aug 2024 21:01:06 +0100 Subject: [PATCH 54/58] build: lint --- lib/success.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/success.js b/lib/success.js index 13287fb2..64c684a7 100644 --- a/lib/success.js +++ b/lib/success.js @@ -87,7 +87,7 @@ export default async function success(pluginConfig, context, { Octokit }) { ); for (const { nodes, pageInfo } of responseAssociatedPRs) { if (nodes.length === 0) continue; - + responsePRs.push(...buildIssuesOrPRsFromResponseNode(nodes, "PR")); if (pageInfo.hasNextPage) { let cursor = pageInfo.endCursor; @@ -191,12 +191,14 @@ export default async function success(pluginConfig, context, { Octokit }) { await Promise.all( uniqBy([...prs, ...issues], "number").map(async (issue) => { + const issueOrPR = issue.pull_request ? "PR" : "issue"; + const canCommentOnIssue = successCommentCondition ? template(successCommentCondition)({ ...context, issue }) : true; if (!canCommentOnIssue) { - logger.log("Skip commenting on issue #%d.", issue.id); + logger.log(`Skip commenting on ${issueOrPR} #%d.`, issue.id); return; } @@ -205,7 +207,6 @@ export default async function success(pluginConfig, context, { Octokit }) { : getSuccessComment(issue, releaseInfos, nextRelease); try { const comment = { owner, repo, issue_number: issue.number, body }; - const issueOrPR = issue.pull_request ? "PR" : "issue"; debug("create comment: %O", comment); const { data: { html_url: url }, @@ -241,18 +242,18 @@ export default async function success(pluginConfig, context, { Octokit }) { } catch (error) { if (error.status === 403) { logger.error( - `Not allowed to add a comment to the ${issueOrPR} #%d.`, + `Not allowed to add a comment to the issue/PR #%d.`, issue.number, ); } else if (error.status === 404) { logger.error( - `Failed to add a comment to the ${issueOrPR} #%d as it doesn't exist.`, + `Failed to add a comment to the issue/PR #%d as it doesn't exist.`, issue.number, ); } else { errors.push(error); logger.error( - `Failed to add a comment to the ${issueOrPR} #%d.`, + `Failed to add a comment to the issue/PR #%d.`, issue.number, ); // Don't throw right away and continue to update other issues From 15c308913e7853b6c23ff858082a53bee420e0e5 Mon Sep 17 00:00:00 2001 From: babblebey Date: Fri, 30 Aug 2024 21:08:04 +0100 Subject: [PATCH 55/58] docs: add ignore bots pr/issues example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb60253d..b72b0d22 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,10 @@ The message for the issue comments is generated with [Lodash template](https://l - do not create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on issues: `"<% return !issue.pull_request; %>"` - to only comment on pull requests: `"<% return issue.pull_request; %>"` +- to avoid comment on PRs or issues created by Bots: `"<% return issue.user.type !== 'Bot'; %>"` - you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant'); %>"` -> check the [GitHub API pull request object](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request) for properties which can be used for the filter +> check the [GitHub API issue object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) for properties which can be used for the filter #### failComment From ff1706630c41486f1a0aa5bdd6f22dfd2d9bb5b7 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 2 Sep 2024 16:47:15 +0100 Subject: [PATCH 56/58] fix: user issue `number` over `id` --- lib/success.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index 64c684a7..a6769bb7 100644 --- a/lib/success.js +++ b/lib/success.js @@ -198,7 +198,7 @@ export default async function success(pluginConfig, context, { Octokit }) { : true; if (!canCommentOnIssue) { - logger.log(`Skip commenting on ${issueOrPR} #%d.`, issue.id); + logger.log(`Skip commenting on ${issueOrPR} #%d.`, issue.number); return; } From 601303d9316c64d286b91affabcb107c1577a9df Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 2 Sep 2024 16:47:53 +0100 Subject: [PATCH 57/58] test: improve case `'Does not comment/label associatedPR and relatedIssues created by "Bots"'` --- test/success.test.js | 106 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/test/success.test.js b/test/success.test.js index aaebc32f..22463310 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -3353,26 +3353,96 @@ test('Does not comment/label associatedPR and relatedIssues created by "Bots"', const prs = [ { number: 2, - pull_request: true, + id: "PR_kwDOMLlZj851SZzc", + title: "pr title", body: "Fixes #4", - state: "closed", - user: { + url: "https://pr-url", + createdAt: "2024-07-13T09:57:51Z", + updatedAt: "2024-08-29T12:15:33Z", + closedAt: "2024-07-13T09:58:50Z", + comments: { + totalCount: 23, + }, + state: "MERGED", + author: { login: "user_login", - type: "User", - avatar_url: "https://some_url.link", - html_url: "https://some_url.link", + url: "https://user-url", + avatarUrl: "https://avatar-url", + __typename: "User", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + mergeable: "UNKNOWN", + canBeRebased: false, + changedFiles: 1, + mergedAt: "2024-07-13T09:58:50Z", + isDraft: false, + mergedBy: { + login: "user", + avatarUrl: "https://alink-to-avatar", + url: "https://user-url", + }, + commits: { + totalCount: 1, }, }, { number: 3, - pull_request: true, + id: "PR_kwDOMLlZj851SZzc", + title: "pr title", body: "Fixes #5", - state: "closed", - user: { - login: "bot_user_login", - type: "Bot", - avatar_url: "https://some_url.link", - html_url: "https://some_url.link", + url: "https://pr-url", + createdAt: "2024-07-13T09:57:51Z", + updatedAt: "2024-08-29T12:15:33Z", + closedAt: "2024-07-13T09:58:50Z", + comments: { + totalCount: 23, + }, + state: "MERGED", + author: { + login: "user_login", + url: "https://user-url", + avatarUrl: "https://avatar-url", + __typename: "Bot", + }, + authorAssociation: "OWNER", + activeLockReason: null, + labels: { + nodes: [ + { + id: "label_id", + url: "label_url", + name: "label_name", + color: "ededed", + }, + ], + }, + milestone: null, + locked: false, + mergeable: "UNKNOWN", + canBeRebased: false, + changedFiles: 1, + mergedAt: "2024-07-13T09:58:50Z", + isDraft: false, + mergedBy: { + login: "user", + avatarUrl: "https://alink-to-avatar", + url: "https://user-url", + }, + commits: { + totalCount: 1, }, }, ]; @@ -3518,15 +3588,6 @@ test('Does not comment/label associatedPR and relatedIssues created by "Bots"', {}, { body: ["released"] }, ) - .postOnce( - `https://api.github.local/repos/${owner}/${repo}/issues/3/comments`, - { html_url: "https://github.com/successcomment-3" }, - ) - .postOnce( - `https://api.github.local/repos/${owner}/${repo}/issues/3/labels`, - {}, - { body: ["released"] }, - ) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( "in:title", @@ -3571,6 +3632,7 @@ test('Does not comment/label associatedPR and relatedIssues created by "Bots"', "https://github.com/issues/1", ), ); + t.true(t.context.log.calledWith("Skip commenting on PR #%d.", 3)); t.true( t.context.log.calledWith( "Added comment to issue #%d: %s", From 367047d87a2d0af5dc0949d53b69d9993f932461 Mon Sep 17 00:00:00 2001 From: babblebey Date: Mon, 2 Sep 2024 17:05:03 +0100 Subject: [PATCH 58/58] doc: modify `buildIssuesOrPRsFromResponseNode` documentation --- lib/success.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/success.js b/lib/success.js index a6769bb7..3479560c 100644 --- a/lib/success.js +++ b/lib/success.js @@ -504,7 +504,7 @@ const loadSingleCommitAssociatedPRs = `#graphql `; /** - * Build associatedPRs (into issue-like object with `pull_request` property) from the GraphQL repository response + * Build associatedPRs or RelatedIssues object (into issue-like object with `pull_request` property) from the GraphQL repository response * @param {object} responseNodes * @param {"ISSUE" | "PR"} type * @returns {object[]}