Skip to content

perf(cicd): batch release-data lookups over GraphQL — ~1,076 requests to ~78, and fix Closes: #N - #37369

Draft
sfreudenthaler wants to merge 2 commits into
refactor-share-release-githubfrom
tech-debt-release-data-graphql
Draft

perf(cicd): batch release-data lookups over GraphQL — ~1,076 requests to ~78, and fix Closes: #N#37369
sfreudenthaler wants to merge 2 commits into
refactor-share-release-githubfrom
tech-debt-release-data-graphql

Conversation

@sfreudenthaler

@sfreudenthaler sfreudenthaler commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closes: #35763

Note

Draft, and stacked on #37387 (the transport extraction), which is itself now based on main since #37361 merged. Base retargets automatically as the stack lands. Not for review until #37387 is in.

Why this is stacked

Because #37387 puts the transport in one place, this migration is written once and both release scripts get it. Before the extraction, the same change would have had to be applied twice — which is precisely the pattern that produced #37201 and #37138.

The perf half

Commit→PR resolution was one REST call per commit. Under merge commits a release carries 5-10x more commits than PRs, so the loop scaled with the wrong number — and both scripts resolve the same range in the same pipeline run:

                    before                                    after
   ┌──────────────────────────────────┐        ┌──────────────────────────────┐
   │ gather-release-data              │        │ gather-release-data          │
   │   compare API              2     │        │   compare API            2   │  REST, unchanged
   │   /commits/{sha}/pulls   485     │        │   associatedPRs         10   │  50 commits/query
   │   /pulls/{n}              51     │  ───►  │   pullRequest            3   │  20 PRs/query
   │ release-qa-status                │        │ release-qa-status            │
   │   compare API              2     │        │   compare API            2   │
   │   /commits/{sha}/pulls   485     │        │   associatedPRs         10   │  shared code path
   │   /pulls/{n}              51     │        │   /pulls/{n}            51   │  needs author/url
   ├──────────────────────────────────┤        ├──────────────────────────────┤
   │ ~1,076 requests                  │        │ ~78 requests                 │
   │ + 35 × sleep(500) ≈ 17s          │        │ no sleeps on the shared path │
   └──────────────────────────────────┘        └──────────────────────────────┘

Those sleep(500) calls were added in #37213 to stay under the secondary rate limit. On the batched path they are unnecessary.

The correctness half — the reason this is worth doing now

gather-release-data extracted linked issues with a body regex requiring whitespace directly after the keyword:

/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi
PR body Old regex GraphQL
Closes #123
Closes: #123
Fixes: #123
closes dotCMS/core#123
Resolves: https://github.com/dotCMS/core/issues/123
linked only via the Development sidebar

Closes: #NNN — with the colon — is the form CLAUDE.md mandates for every dotCMS PR, because the link-issue merge gate requires it. The house style was the one form that didn't match.

closingIssuesReferences reads the relationship GitHub itself renders, so the regex is deleted rather than patched. (release-qa-status already used this API — it was ahead on linked issues and behind on commit→PR resolution, which is what interleaved drift looks like.)

Verification

Both scripts run over v26.08.24-01...v26.09.02-01 (485 commits, 51 PRs):

REST GraphQL
PRs resolved 51 51 — identical set
Titles / labels / categories identical
PRs with a linked issue 25 / 51 48 / 51
Issue refs total 27 50
Requests (both scripts) ~1,076 ~78

release-qa-status's markdown report is unchanged apart from one issue label GitHub's own release automation edited between the two runs (Next ReleaseRelease : 26.09.03-01).

The PR set, titles, labels and categories match exactly — for those fields this is a pure transport swap. The only behavioural change is linked issues, and it is a strict improvement. The three PRs still without a link genuinely close nothing.

Don't read this as a speed PR. Wall clock only moved 51s → 38s locally, where network latency dominates and CI runners are faster. The value is ~78 requests instead of ~1,076 inside the release pipeline, and the linked-issue fix.

Tests

76 across the three packages. The GraphQL transport tests live in shared/ and now cover both scripts at once: merge/branch commit dedup, unmerged-PR filtering, batch arithmetic (120 commits → 3 queries, not 120), oid validation rejecting an alias-injection attempt, and error propagation instead of silent batch loss. gather-release-data keeps its own tests for cross-repo ref filtering, body truncation and null body/labels.

Also re-adds draft-filtering coverage for listStandardReleaseTags (#37138), which #37140 shipped untested — now one test covers both scripts.

Deliberately unchanged

  • Compare API stays REST — no clean GraphQL equivalent for a merge-base range with pagination.
  • listStandardReleaseTags stays REST, so @octokit/plugin-throttling stays with it. It still paginates the full release list, which is dozens of requests given the CLI and LTS tag volume.
  • release-qa-status's fetchPRDetails stays REST. It needs author / authorType / url for the Slack digest and is only ~7-51 calls; batching it is a separate change with its own review surface.
  • Errors are re-thrown, not warned-and-skipped. An errored batch is indistinguishable from a batch with no PRs, and swallowing it would quietly shrink the changelog — the exact failure mode Release notes & QA status: replace (#N) squash-subject regex with commits→PRs API #37201 existed to kill.

🤖 Generated with Claude Code

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sfreudenthaler's task in 1m 46s —— View job


Review

Read all four changed files and traced the two GraphQL paths against their tests. This is a clean, well-reasoned transport swap — injection is properly guarded (sha /^[0-9a-f]{40}$/i and Number.isInteger(n) && n > 0 before interpolation, both tested), the re-throw semantics are deliberate and correct, and the null/absent handling on both paths is sound. One thing worth a look before merge.

New Issues

  • 🟡 Medium: .github/scripts/shared/github.ts:229associatedPullRequests(first: 5) narrows what the old REST path fetched with per_page: 100. Merged PRs beyond the 5th associated PR for a single commit are dropped silently, and a shrunken changelog is precisely the failure mode #37201 exists to prevent.
    • Assumption: a default-branch commit almost always maps to exactly one merged PR, so 5 is ample in practice (your v26.08.24-01...v26.09.02-01 run produced an identical PR set, which supports this).
    • What to verify: that no commit in a real release range associates with >5 PRs where a merged one sits past index 5 (e.g. a commit carried across several long-lived branches). If that's plausible, bump to first: 20 or higher — the node-budget cost is negligible and it removes the silent-truncation risk entirely. Fix this →

Notes (non-blocking, no action needed)

  • fetchPRDetails accesses pr.closingIssuesReferences.nodes without optional chaining while using pr.labels?.nodes ?? [] for labels. Consistent with the GraphQL schema (connections are non-null), so this is fine — just calling out the asymmetry in case it was unintentional.
  • resolvePRNumbers guards if (!node) continue for a null object, which is correct for a not-found oid. It relies on every input sha resolving to a Commit (true — they come from fetchCommitRange); a sha resolving to a non-Commit git object would make node.associatedPullRequests undefined. Not reachable with the current caller, so no change needed.

Test coverage for the new behavior is thorough — batch arithmetic, dedup, unmerged filtering, cross-repo filtering, oid-injection rejection, error propagation, and body/label null handling are all exercised. Nice work.

· tech-debt-release-data-graphql

Commit->PR resolution was one REST call per commit: 485 for a full
release, and both release scripts resolve the same range in the same
pipeline run, so the release pipeline spent ~1,076 calls on it. Batched
over GraphQL aliases in the shared transport (50 commits/query) that
becomes 20, and the 35 mandated half-second sleeps go away.

The bigger win is correctness. gather-release-data now reads linked
issues from `closingIssuesReferences` instead of regex-matching the PR
body. The regex required whitespace directly after the keyword, so
`Closes: #N` — the form CLAUDE.md mandates for every dotCMS PR, because
the link-issue merge gate requires it — never matched. On v26.09.02-01
that cost 23 of 51 PRs their issue cross-link.

Verified over v26.08.24-01...v26.09.02-01: identical PR set, titles,
labels and categories; linked issues 25/51 PRs -> 48/51. The
release-qa-status markdown report is unchanged apart from one issue
label GitHub's own automation edited between runs.

Closes: #35763

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sfreudenthaler
sfreudenthaler force-pushed the tech-debt-release-data-graphql branch from 062869f to ae781a7 Compare September 3, 2026 21:03
#37140 fixed the draft-ordering and duplicate-tag hazards but landed
tests only for findPreviousTag's hasNotes walk-back. The
`if (release.draft) continue;` guard is load-bearing: dotCMS/core has 6
draft releases matching the standard tag pattern, one of which
duplicates a published tag, so an unfiltered list both mis-orders the
walk-back and makes findIndex ambiguous.

Now that the transport is shared, one test covers both scripts.

Refs: #37138

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sfreudenthaler
sfreudenthaler changed the base branch from main to refactor-share-release-github September 3, 2026 21:04
@sfreudenthaler sfreudenthaler changed the title perf(cicd): batch release-data lookups over GraphQL — 538 requests to 13, and fix Closes: #N perf(cicd): batch release-data lookups over GraphQL — ~1,076 requests to ~78, and fix Closes: #N Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant