Repository navigation
next-release: commit-range races the API-pushed bump commit, silently emptying the release-PR changelog and crashing the labels step #262
Description
Summary
On a next → master release-PR refresh where a fresh chore: bump version commit is pushed, get-pr-commit-range can compute a commit range whose head sha was never fetched into the job's local git checkout — because the bump commit that just became the PR's head was created entirely via the GitHub API, not via local git. Every downstream step that runs git log against that range then either silently produces an empty result (the changelog generator, which patches the PR with a "successful" but content-free body) or crashes outright (the raw commit-type-detection shell step), failing the whole refresh job.
This is live in production, not theoretical — reproduced on CLDMV/droidsock's persistent release PR.
Observed symptom
droidsock PR #19 ("release: v1.2.0 - add device discovery") currently has an essentially empty body — every changelog section reads its placeholder text ("No new features", "No bug fixes", "No other changes") — despite the release bundling real feat:/fix:/ci: commits from three merged PRs (#17, #18, #20). All CI on the release PR is green, so nothing visibly flags that the body is wrong.
Root cause
Three files in CLDMV/.github (all pinned @v4, from this workflow chain: workflow-next-release.yml → update-release-pr/action.yml):
1. github/api/commit/action.mjs ("Create signed commit" step) creates the version-bump commit and moves the branch ref entirely via the GitHub REST API — POST /git/blobs, POST /git/trees, POST /git/commits, PATCH /git/refs/heads/{branch}. It never runs git push, and nothing afterward runs git fetch. The job's local git checkout has no idea this commit exists.
2. github/steps/get-pr-commit-range/action.mjs, which runs later in the same job, re-resolves the range with a fresh live API call rather than anything derived from the local checkout:
const pr = await api("GET", `/pulls/${prNumber}`, null, { token, owner, repo });
const commitRange = `${pr.base.sha}..${pr.head.sha}`;Because the PR's head branch is next (this is the persistent release PR), once the ref-update PATCH from step 1 has propagated, this GET returns the brand-new bump commit as pr.head.sha — a sha that was never fetched into this job's local .git.
3. update-release-pr/action.yml's "Compute commit types in PR range" step consumes that range with a raw, unguarded git command:
run: |
types=$(git log --no-merges --format='%s' "$RANGE" \
| { grep -vE '^(release|chore: bump version)' || true; } \
...When $RANGE's head sha is unresolvable locally, this fails hard:
fatal: Invalid revision range 9e2c95c546d5fb1c5ec3ec614fe6fba797d4d71b..d04fd8c77fe982bb27b633823dec0b873d1fcdfe
##[error]Process completed with exit code 128.
— confirmed 9e2c95c is a real, valid, reachable commit (release: v1.1.1 … on master) in a normal full clone; the range itself is legitimate, only this job's particular checkout never has the newer endpoint.
The same broken range is also passed to generate-comprehensive-changelog (the "Generate updated changelog" step, which runs before the failing step). That action apparently tolerates the unresolvable range without erroring, producing a changelog with every section emptied — and the workflow still logs the update as a success:
✅ PR #19 has been updated with title 'release: v1.2.0 - add device discovery (subnet TCP sweep + mDNS)' and latest changelog
✅ Preserved 1 sticky block(s) in the PR body
This is the more damaging half of the bug: the hard crash at least fails the job loudly; the empty changelog ships silently as a "successful" update.
Why it's intermittent
This is a check-then-act race, not a deterministic failure: whether GET /pulls/{n} (step 2) reflects the just-updated ref from the PATCH (step 1) depends on propagation timing at the moment the job happens to query it. On CLDMV/droidsock, run #33703465364 (PR #17's merge, which also pushed a bump commit) succeeded; run #33755241538 (PR #18's merge, minutes later) hit the race and both silently emptied the changelog and then failed on the labels step. Any repo on the v4 flow that bumps a fresh version on a refresh (i.e. bump-already-applied != 'true') is exposed to this on every such refresh — it's a matter of timing, not a rare edge case.
Evidence
- Failing run:
33755241538, job100648040459("release / 🔁 Refresh release PR release: v3.3.0 - add compute-highest-commit-type foundation action (v4… #19"), step "Refresh release PR" → sub-actionrefresh.pr-types. - The same job's own logged API response bodies show the PR body transitioning, within this one run, from a correctly-populated changelog (bug fixes under release: v3.2.3 - allow ' - <subject>' suffix on release commits #17, other changes under release: v3.4.0 - add local-pr-title-normalizer workflow (v4 §11 PR 2) #20) to the fully-empty placeholder body immediately before the crash — i.e. the empty body was written by this run, not inherited from an earlier one.
- Locally confirmed:
9e2c95c546d5fb1c5ec3ec614fe6fba797d4d71bisrelease: v1.1.1 - pad slashes between adjacent code spans (#16)onmaster, and is a real ancestor ofnext— the base side of the range is fine. - Current state of PR #19: title correctly resolved to
release: v1.2.0 - add device discovery (subnet TCP sweep + mDNS)(derived from the highest-bump commit, unaffected), body's changelog sections are all empty.
Suggested fix directions (not prescriptive)
- Have
get-pr-commit-rangeresolvehead.shafrom something guaranteed to be fetched locally (e.g.git rev-parse HEADafter step 1, or an explicitgit fetch origin <head-ref>immediately after the bump-commit ref update) instead of a fresh live APIGETthat can race the API-only push. - At minimum, add error handling around the raw
git log ... "$RANGE"in "Compute commit types in PR range" so an unresolvable range fails loudly with a clear message instead of a barefatal:/exit 128 — and havegenerate-comprehensive-changelogdo the same rather than silently rendering an empty-but-"successful" changelog. - Consider whether step 1's API-only commit creation should end with a local
git fetch/git reset --hard <new-sha>so every subsequent step in the same job operates on a checkout that actually reflects what was just pushed.
Activity
cldmv-bot commentedon Sep 6, 2026
Closed in v4.26.0 — resolved by #265.