From 8c6201d74524e99d00961bfe4890782df0dc6dd4 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Wed, 2 Sep 2026 14:51:16 -0500 Subject: [PATCH 1/3] fix: paginate listReleases and halt when no previous release is found The previous-release lookup called `listReleases` without pagination, so only the 30 most recent releases were scanned. GitHub orders releases by creation date descending, and a release published from a draft sorts by the date the draft was created rather than the date it was published, so a release can sit well beyond the first page. When the current release is not on that page the loop never sets `currentReleaseFound` and `previousRelease` stays null. `core.setFailed` does not halt execution, so the null `previousRelease` was then dereferenced by `compareCommits`, and the resulting `TypeError: Cannot read properties of null (reading 'tag_name')` masked the actionable message that had just been emitted. Paginate with `per_page: 100` and return after `setFailed` so the real message is the failure. --- action.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index f608e77..338a2b3 100644 --- a/action.yml +++ b/action.yml @@ -120,15 +120,18 @@ runs: } // Find a previous release in order to find commits and later use them to find associated pull requests. - releases = await github.rest.repos.listReleases({ + // This must be paginated: releases are returned newest-first by creation date (not publication date), + // so a release published from a long-lived draft can sit well beyond the first page. + releases = await github.paginate(github.rest.repos.listReleases, { owner: context.repo.owner, - repo: context.repo.repo, + repo: context.repo.repo, + per_page: 100, }); previousRelease = null; currentReleaseFound = false; console.log(`Looking for a release prior to ${currentTag}...`); - for (release of releases.data) { + for (release of releases) { if (currentReleaseFound && (release.target_commitish != currentRelease.target_commitish || !release.target_commitish.match(/[0-9a-f]{40}/))) { // In test scenarios where a positive test scenario release and a negative test scenario are created at from the same target_commitish, // we want to avoid setting this release as the previous (base) release, as no commits will be found when comparing the two. @@ -143,10 +146,14 @@ runs: } if (previousRelease == null) { + // 'core.setFailed' does not halt execution, so return explicitly. Otherwise the 'compareCommits' call + // below dereferences a null 'previousRelease' and the resulting TypeError masks the message above. core.setFailed(`This action requires that at least one release prior to ${currentTag} exists, regardless of "include_regex".`); - } else { - console.log(`Found a prior release to "${currentTag}": "${previousRelease.tag_name}"`); + return { + "comments": [] + }; } + console.log(`Found a prior release to "${currentTag}": "${previousRelease.tag_name}"`); // Compare commits, as a pre-requisuite for finding associated pull requests. commitsResponse = await github.rest.repos.compareCommits({ From c45cbf738760460dc8fb2275136ca65839d56b7e Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Wed, 2 Sep 2026 14:51:48 -0500 Subject: [PATCH 2/3] fix: prefer a previous release that also matches include_regex The previous-release lookup took the first eligible release in the list regardless of `include_regex`. In a monorepo with per-module tags that is usually an unrelated module, so `compareCommits` runs over an arbitrary range and comments on whatever pull requests happen to fall inside it - silently, with no error. Concretely, for `dw-cc-ps-posting-service-database@0.1.0` the next release is skipped by the same-SHA guard and the base becomes `dw-cc-ps-spring-boot-starter@0.1.0-rc.1`, comparing a database release against a spring-boot-starter release candidate. Prefer the first eligible release whose tag also matches `include_regex`. When no prior release matches, warn and fall back to the previously selected release, so repos cutting a module's first release - and this action's own test workflows, whose throwaway prereleases are deleted on teardown - behave exactly as before. With the default `include_regex` of `.*` this is a no-op. --- action.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 338a2b3..6390dd5 100644 --- a/action.yml +++ b/action.yml @@ -129,6 +129,7 @@ runs: }); previousRelease = null; + previousReleaseFallback = null; currentReleaseFound = false; console.log(`Looking for a release prior to ${currentTag}...`); for (release of releases) { @@ -138,13 +139,28 @@ runs: // However, we obviously want to account for normal scenarios where the target_commitish of two releases is the same and is not a SHA, // for example two releases with target_commmitish as 'main'. This is seemingly the best way to infer this given the limited information // returned by the listReleases endpoint. - previousRelease = release; - break; + if (previousReleaseFallback == null) { + previousReleaseFallback = release; + } + // Prefer a previous release whose tag also matches 'include_regex'. In a monorepo with per-module + // tags the immediately preceding release usually belongs to an unrelated module, which would make + // the comparison range - and therefore the pull requests commented on - arbitrary. + if (release.tag_name.match(pattern)) { + previousRelease = release; + break; + } } else if (release.tag_name == currentTag) { currentReleaseFound = true; } } + if (previousRelease == null && previousReleaseFallback != null) { + // Preserve the previous behaviour when 'include_regex' matches no prior release at all, so that repos + // releasing a module for the first time (and this action's own test workflows) keep working. + core.warning(`No release prior to ${currentTag} matches the supplied 'include_regex'. Falling back to "${previousReleaseFallback.tag_name}".`); + previousRelease = previousReleaseFallback; + } + if (previousRelease == null) { // 'core.setFailed' does not halt execution, so return explicitly. Otherwise the 'compareCommits' call // below dereferences a null 'previousRelease' and the resulting TypeError masks the message above. From d72287995fffd46377d50054f12629a549d7f1ef Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Wed, 2 Sep 2026 15:08:54 -0500 Subject: [PATCH 3/3] refactor: scope release lookup variables with const/let The previous-release lookup assigned to undeclared variables, creating implicit globals that would break under strict-mode execution. Declare the variables in this block explicitly. Limited to the block this change already rewrites; the rest of the script still relies on implicit globals and is left for the TypeScript rewrite noted at the top of the file. --- action.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index 6390dd5..03da9da 100644 --- a/action.yml +++ b/action.yml @@ -122,17 +122,17 @@ runs: // Find a previous release in order to find commits and later use them to find associated pull requests. // This must be paginated: releases are returned newest-first by creation date (not publication date), // so a release published from a long-lived draft can sit well beyond the first page. - releases = await github.paginate(github.rest.repos.listReleases, { + const releases = await github.paginate(github.rest.repos.listReleases, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100, }); - previousRelease = null; - previousReleaseFallback = null; - currentReleaseFound = false; + let previousRelease = null; + let previousReleaseFallback = null; + let currentReleaseFound = false; console.log(`Looking for a release prior to ${currentTag}...`); - for (release of releases) { + for (const release of releases) { if (currentReleaseFound && (release.target_commitish != currentRelease.target_commitish || !release.target_commitish.match(/[0-9a-f]{40}/))) { // In test scenarios where a positive test scenario release and a negative test scenario are created at from the same target_commitish, // we want to avoid setting this release as the previous (base) release, as no commits will be found when comparing the two.