Release notes & QA status: replace (#N) squash-subject regex with commits→PRs API - #37213
Conversation
…ject regex Both release scripts extracted PR numbers by matching `(#N)$` against commit subjects, which only works under squash merging. dotCMS removed squash merging after the aug-24 incident, so the compare range between release tags now contains merge commits plus every feature-branch commit. Two failure modes follow: merge commits whose subject has no `(#N)` drop their PR entirely, and feature-branch subjects ending in an ISSUE number (e.g. `(#37132)`) get fed to `pulls.get` as a fake PR number, which 404s. Replace `extractPRNumbers` with an async `resolvePRNumbers` in both packages that calls `GET /repos/{owner}/{repo}/commits/{sha}/pulls` (`octokit.repos.listPullRequestsAssociatedWithCommit`) per commit, filters to `merged_at`, and dedupes through a Set. The API maps a merged PR's branch commits and its merge commit to the same PR, so dedup is free, and it returns `[]` for direct pushes. Batched at 15 with `Promise.all` and a 500ms inter-batch sleep, mirroring the existing `fetchPRDetails` pattern; a per-commit try/catch warns to stderr so one bad sha cannot abort the range. `CommitInfo.message` was read only by the old regex, so it and the message-building line in `fetchCommitRange` are removed. In release-qa-status the "has the merge strategy changed?" stderr warning is deleted — it existed solely to flag the squash assumption this change removes. No workflow changes: `pull-requests: read` is already granted where these run. Closes: #37201 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the redundant per-commit-failure test and stderr spy, the duplicate resolvePRNumbers test block in release-qa-status (mirror is covered by gather-release-data), a dead ordering comment, and trim the docblock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @sfreudenthaler's task in 2m 0s —— View job Code Review
New Issues
Notes (not issues)
No 🔴 Critical or 🟠 High issues. The two 🟡 Medium items are non-blocking. The change is sound and the regression coverage for |
…rade test resolvePRNumbers issues one request per commit. Under merge commits a release range is 118-228 commits (measured on v26.08.28-01...v26.08.31-01 and v26.08.24-01...v26.08.28-01), and BATCH_SIZE=15 with sleep(500) between batches puts us over GitHub's ~900/min REST secondary limit even though we are well inside the 5000/hr primary budget. A 403 there is swallowed by the per-commit catch and degrades into a PR silently missing from the release notes -- the exact failure this change set exists to remove. Wire @octokit/plugin-throttling into the single createOctokit() choke point in both packages so GitHub's own plugin queues requests and honors retry-after, bounded at MAX_RATE_LIMIT_RETRIES=3 so an exhausted quota cannot park a release job indefinitely. Also restores the "one failing sha does not abort the batch" test cut in fe05fea. That catch is the only untested branch in resolvePRNumbers and it fails silently; removing the try/catch fails exactly this test, verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressing the three findings from the automatic review — all three were fair, and one turned out to be a live problem. 1 & 2 (test coverage vs. the description) — correct, and the cause was a stale PR body rather than missing work. Commit Resolved as follows:
3 (API-call volume) — measured, and the primary-budget concern is a non-issue while the secondary limit was a real bug:
That is 2–5% of the 5000/hr primary budget — fine. But Fixed by wiring On the rejected merge-commit fast path: you're right that it cuts calls to the number of PRs, not commits. On the range above it returns the identical 8 PRs at 1/5 the cost. Still rejected, but for a better reason than the original body gave: any range spanning the squash→merge transition contains single-parent squash commits, which a two-parent filter drops silently. Same failure class. Also worth noting for reviewers: the premise is no longer hypothetical. |
listPullRequestsAssociatedWithCommit defaults to 30 items/page. A commit associated with more merged PRs than that would drop the remainder silently -- the same failure class this change set removes. Not reachable from our inputs: we only pass commits from a main..main compare range, and for a commit present in the default branch the endpoint returns the single merged PR that introduced it (measured 1 for all 40 sampled commits of v26.08.28-01...v26.08.31-01). The >30 case needs a commit absent from main, where the endpoint returns open PRs, which the merged_at filter already drops. per_page: 100 closes the class for one line per copy, rather than pulling in octokit.paginate for a case that cannot arise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Added Worth recording how narrow the exposure actually is, so it doesn't get re-litigated: the endpoint's documented behavior is that for a commit present in the default branch it returns the merged pull request that introduced the commit. Measured across So this is belt-and-braces rather than a live bug. Took it anyway: one line per copy closes a silent-truncation class for free, and
|
At the give-up call (retryCount === MAX_RATE_LIMIT_RETRIES) the log line was
built unconditionally before the bound was applied, so it printed "retry 4/3"
and then returned false. Cosmetic, but it only ever surfaces in stderr during
real rate-limit exhaustion -- precisely when the log is being read.
Compute willRetry once, branch the message on it, return it. The onThrottle
test now asserts both message shapes ("retry 1/3", "retry 3/3", "giving up
after 3 retries", and never "retry 4/"); restoring the old unconditional
message fails it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Fixed in 0259189 — real bug in code I added in this PR, and the suggested rewrite was right, so I took it as written in both copies. The reason it's worth more than "cosmetic": that line only ever prints during genuine rate-limit exhaustion, which is exactly when someone is reading stderr to work out what happened. Also tightened the
|
|
dotbot code review:
Core API change (regex to commits->pulls) is sound, tested, and verified against real release ranges. The rate-limit hook retry timing is a P2 behavioral/log-accuracy issue that degrades retry usefulness but is bounded and not an output-corrupting correctness bug, so it does not warrant marking the patch incorrect. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · deepseek/deepseek-v4-pro-0813 · medium |
|
dotbot code review:
The change correctly replaces the squash-merge commit subject regex with the commits-to-pulls GitHub API and handles deduplication cleanly. The Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · google/gemini-3.8-flash · medium |
|
dotbot code review:
Verified the full diff end-to-end: Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · ~z-ai/glm-latest · medium |
Why
.github/scripts/gather-release-data/src/github.tsand.github/scripts/release-qa-status/src/github.tsboth extracted PR numbers from commit subjects with the same\(#(\d+)\)\s*$regex. That only holds under squash merging.Squash merging is now disabled on this repo (
allow_squash_merge: false), so PRs land as merge commits and the compare range between two release tags contains merge commits and every feature-branch commit underneath them. Two things go wrong:Merge pull request #N from …, or a custom merge title) doesn't end in(#N), so its PR is never resolved. It drops out of the AI release notes and out of the QA-status section.release-qa-statuswarned about this atindex.ts:272;gather-release-datawas silent.mainverbatim, and their subjects frequently end in an issue number the author typed. The old regex hands that topulls.get, which 404s.Measured against a real release range
v26.08.28-01...v26.08.31-01, 118 commits, 24 merge commits:(#N)$regexpulls.get: #36197, #36720, #36845, #36855, #37066commits/{sha}/pullsAPIA 38% garbage rate on the most recent release cut. Every release since squash was disabled has run through this path.
What changed
extractPRNumbersis replaced in both packages by an asyncresolvePRNumbers(octokit, owner, repo, commits)that resolves each commit throughGET /repos/{owner}/{repo}/commits/{sha}/pulls(octokit.repos.listPullRequestsAssociatedWithCommit):pr.merged_at— for commits not reachable from the default branch the endpoint also returns open PRs.Set. The API maps a merged PR's branch commits and its merge commit to the same PR number, so branch/merge dedup is free.per_page: 100, so the default 30-item page can never truncate. A default-branch commit resolves to the single merged PR that introduced it (measured: 1 for all 40 sampled commits ofv26.08.28-01...v26.08.31-01), so page 1 always suffices — nooctokit.paginateneeded.BATCH_SIZE = 15withPromise.allandsleep(500)between batches, reusing the existingfetchPRDetailsconcurrency pattern in the same files.try/catchwrites a warning to stderr and returns[], so one unresolvable sha cannot abort the whole range — consistent with howfetchPRDetailsalready degrades to partial results.Rate limiting
One request per commit means 118–228 requests per release range (measured: 118 across
v26.08.28-01...v26.08.31-01, 228 acrossv26.08.24-01...v26.08.28-01). That is only 2–5% of the 5000/hr primary budget, butBATCH_SIZE = 15withsleep(500)between batches puts us at roughly 1200+ req/min against GitHub's ~900/min REST secondary limit. A 403 there gets swallowed by the per-commitcatchand degrades into a PR silently missing from the release notes — the exact failure this PR exists to remove.So
@octokit/plugin-throttling(GitHub's own plugin) is wired into the singlecreateOctokit()choke point in each package. It queues requests and honorsretry-afterfor both the primary and secondary limits. Retries are bounded atMAX_RATE_LIMIT_RETRIES = 3so an exhausted quota cannot park a release job indefinitely.No regex fast path. A "two-parent commits only" shortcut was considered and rejected. On the range above it happens to return the identical 8 PRs at 1/5 the API cost — but any range spanning the squash→merge transition contains single-parent squash commits, which it would silently drop. That is the same class of silent loss this PR removes.
Supporting cleanups
CommitInfo.messagewas read only by the old regex, so the field (types.ts) and the message-building line infetchCommitRangeare both removed in both packages.release-qa-status/src/index.ts: the 11-line "has the merge strategy changed? expected squash-merge commit subjects" stderr warning is deleted, not reworded — it existed solely to flag the squash assumption this PR removes, and the line above it already printsResolved N merged PRs from M commits.release-qa-status/src/github.ts: the stale comment infetchClosingIssueRefsthat justified GraphQL alias interpolation by citing "extractPRNumbers' strict regex" now cites the API. TheNumber.isInteger(n) && n > 0belt-and-suspenders filter stays.The two scripts remain independent parallel copies, per the existing
Mirrors the patterns in .github/scripts/gather-release-data/src/github.tsheader — no shared module was extracted. Both must land together or the documented mirror pair goes out of sync.No workflow changes needed.
cicd_comp_ai-release-notes-phase.yml:41already grantspull-requests: read, which covers this endpoint, and thereportjob incicd_6-release.ymlalready callspulls.getwith the same scope.Testing
gather-release-data—npm test→ 36/36 passing,npx tsc --noEmitclean,npm run buildclean. The three oldextractPRNumbersregex tests are replaced by:aaa(feature-branch commit whose subject ended in an issue number),bbb(its two-parent merge commit),ccc(direct push). Asserts[37196]and 3 API calls, covering merge-commit resolution, branch/merge dedup, and the direct-push case in one shot.merged_at: null).process.stderr.writespy sees the failing sha. Mutation-checked: deleting thetry/catchfromresolvePRNumbersfails this test and only this test (1 failed, 34 passed).onThrottlereturnstruebelowMAX_RATE_LIMIT_RETRIESandfalseat it, so the throttling plugin cannot retry forever. Also asserts the log shapes (retry 1/3,retry 3/3,giving up after 3 retries, and neverretry 4/), since the give-up line is only ever read during real rate-limit exhaustion. Mutation-checked: restoring the unconditional message fails it.release-qa-status—npm test→ 40/40 passing,npx tsc --noEmitclean,npm run buildclean. NoresolvePRNumberstest here: it is a documented verbatim mirror of thegather-release-datacopy, and duplicating the fixture buys coverage of the same code twice. The existingfindPreviousTagdrift guard already fails if the two files diverge on release-boundary resolution.Octokit is stubbed inline with
jest.fnin both suites — no network, no new test harness. All fixtures are ≤3 commits so the inter-batchsleep(500)never runs and no fake timers are needed.One new dependency (
@octokit/plugin-throttling, official, in both packages). No behavior change beyond PR-number resolution and rate-limit handling.Closes: #37201
Part of dotCMS/private-issues#673 — item 3 of 4. Item 1 (#37203) is closed, item 2 (#37200) landed in #37219, item 4 (#37202) is #37342.
🤖 Generated with Claude Code
This PR fixes: #37201