fix(release): stream git log so release notes survive long tag spans - #6031
fix(release): stream git log so release notes survive long tag spans#6031M3gA-Mind wants to merge 1 commit into
Conversation
How this change flows2 changed behaviours across 13 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 53 further behaviours left out to keep the diagram readable. flowchart LR
n0["main<br/>changed"]:::changed
n1["runGh<br/>changed"]:::changed
n2["from"]:::impacted
n3["repo"]:::impacted
n4["buildReleasePayload"]:::impacted
n5["options"]:::impacted
n6["commits"]:::impacted
n7["parseArgs"]:::impacted
n0 -->|uses| n2
n0 -->|uses| n3
n0 -->|uses| n5
n0 -->|calls| n7
n1 -->|uses| n5
n2 -->|uses| n3
n2 -->|uses| n5
n3 -->|uses| n5
n4 -->|uses| n2
n4 -->|uses| n3
n4 -->|uses| n6
n6 -->|uses| n2
n7 -->|uses| n5
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe release-note generator now streams ChangesRelease-note generation
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to This change streams release-note Git history to avoid buffer failures, updates related CI coverage, and corrects fallback messaging. No current merge-readiness risk is identified. Sequence Diagram(s)sequenceDiagram
participant ReleaseWorkflow
participant ReleaseNotes
participant Git
ReleaseWorkflow->>ReleaseNotes: run release-note generation
ReleaseNotes->>Git: stream git log output
Git-->>ReleaseNotes: return commit records
ReleaseNotes-->>ReleaseWorkflow: return release notes or fallback outcome
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. (1 skipped: 1 unsupported.)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b130e2e244
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // collector must not have a fixed output ceiling at all. | ||
| const MAX_BUFFER_BYTES = 1024 * 1024; | ||
| const repo = mkdtempSync(join(tmpdir(), 'release-notes-enobufs-')); | ||
| t.after(() => rmSync(repo, { recursive: true, force: true })); |
There was a problem hiding this comment.
Restore the cwd before removing the fixture
On Windows, this test fails during cleanup because node:test runs t.after hooks in registration order: this hook attempts to delete repo while it is still the process's current working directory, and the later hook restoring previousCwd has not run yet. Windows does not allow removal of the active cwd, so move the cwd restoration ahead of the rmSync call or combine both operations into one cleanup hook.
Useful? React with 👍 / 👎.
`generate-release-notes.mjs` read `git log` through `execFileSync`, which
buffers the child's entire stdout and throws `spawnSync git ENOBUFS` past
Node's 1 MiB `maxBuffer` default. Release Production for v0.63.21 died there:
[release-notes] Collecting tinyhumansai/openhuman changes from v0.63.12 to v0.63.21
[release-notes] spawnSync git ENOBUFS
The range is v0.63.12..v0.63.21 because nothing has published since v0.63.12,
and the collector's output grows with the span: 394 KB at v0.63.17 (which
released fine on 2026-08-21) against 1,178,143 bytes at v0.63.21 — 112% of the
buffer. Raising `maxBuffer` would only move the wall, since the span keeps
growing; stream the output instead so there is no ceiling to cross.
- `collectCommits` and `priorAuthorKeys` now stream git's stdout through an
incremental record splitter that carries partial records across chunk
boundaries. `priorAuthorKeys` walks the full history and so was unbounded by
construction, not just by span.
- The fallback step's warning claimed the AI step "failed or timed out". Both
steps died on the same ENOBUFS; `continue-on-error` hid it and the wording
misdirected the investigation. Report the step outcome instead of a cause.
- ci-lite's `scripts` filter matched `scripts/*.mjs`, one directory above this
file, so its existing node --test suite never ran when it changed.
b130e2e to
2abc5d7
Compare
Summary
git login the release-notes generator instead of buffering it, fixing thespawnSync git ENOBUFSthat has blocked Release Production since v0.63.12.priorAuthorKeys, which walks the entire repo history and so was unbounded by construction, not just by release span.scriptslane forscripts/release/*.mjs, whosenode --testsuite could not run on changes to the file it covers.Problem
Run 33753636908 (branch
release, sha399968801) failed in Prepare GitHub release:execFileSyncbuffers the child's whole stdout and throwsENOBUFSpast Node's 1 MiBmaxBufferdefault. The offending line was incollectCommits:This ratchets.
--from latest-releaseresolves to the last published release, so each release that fails to publish lengthens the next range. Measured against the real tags:git logbytesv0.63.12..v0.63.17(2026-08-21)v0.63.12..v0.63.21(2026-09-03)No GitHub Release has been created since v0.63.12 on 2026-08-07;
v0.63.17,v0.63.20andv0.63.21are tagged with nothing published behind them.Solution
collectCommitsandpriorAuthorKeysnow stream git's stdout throughspawnand an incremental record splitter, parsing each\x1e-delimited record as it arrives. There is no output ceiling left to cross, so the next long span cannot regress this again.Why not just raise
maxBuffer. It only moves the wall. The span grows with every unpublished release andpriorAuthorKeysgrows with every commit ever made, so any constant picked today is a future outage with a longer fuse. Streaming removes the limit rather than raising it, and costs one small helper.The splitter carries the tail of each chunk into the next one — a chunk boundary landing inside a record is the failure mode a naive per-chunk
split()gets wrong, and it is covered by its own test.parseGitLogkeeps its existing exported signature and behaviour; it and the streaming path now share oneparseCommitRecord.Not fixed here (filed in the issue as follow-up):
collectPullRequestsmakes one sequentialgh pr viewper PR — 197 for this range. The end-to-end run below took 1m51s before the OpenAI call, nearly all of it those fetches, so the AI step'stimeout-minutes: 5does still fit today with ~3 minutes to spare. That headroom shrinks as the span grows, though: it is a second, slower ratchet behind the one this PR removes.Impact
git logoutput.async; the single call site inmain()(alreadyasync) awaits them.Submission Checklist
scripts/__tests__/generate-release-notes.test.mjsgains two tests: a chunk-boundary test for the splitter, and an end-to-end test that builds a fixture repo whosegit logoutput exceeds 1 MiB and asserts every commit is collected. Both were revert-checked (see below).scripts/**is outside both instrumented trees. The changed lines are covered by thenode --testsuite in thescriptslane, which this PR also arms for this directory.docs/TEST-COVERAGE-MATRIX.md.## Related— no matrix feature IDs apply.git, which the script already invoked.docs/RELEASE-MANUAL-SMOKE.mdcovers built artefacts; this changes how release-note text is generated, not what ships.Closes #NNN— see## Related.How this was verified
node --test scripts/__tests__/generate-release-notes.test.mjs— 12/12 pass (was 10 tests before this PR).execFileSyncversion ofcollectCommitswith the new test in place fails with the production error verbatim:chunk.split(separator)fails the boundary test (actualdrops records when fed one byte at a time).generate-release-notes.mjs --from v0.63.12 --to v0.63.21 --repo tinyhumansai/openhuman --no-ai, i.e. the exact command the failing step ran. Exit 0 in 1m51s, writing 19,036 bytes of notes covering 197 PRs across 6,973 commits. The unfixed script died 0.14s into this same command.Related
gh pr viewcalls incollectPullRequestsbefore they grow into the AI step's 5-minute timeout (tracked in Release Production cannot publish: release-notes generator dies with spawnSync git ENOBUFS on long tag spans #6030).releaseto unblock the next production cut. This PR targetsmainper branch policy; the maintainer should promote or cherry-pick it ontorelease— I have not pushed torelease.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/release-notes-git-log-enobufsb130e2e24Validation Run
pnpm --filter openhuman-app format:check— no files underapp/changed;format/lintin this repo are scoped toopenhuman-app.pnpm typecheck— no TypeScript changed; the edited files are.mjs, YAML, and anode --testsuite.node --test scripts/__tests__/generate-release-notes.test.mjs(12/12), plus the two revert-checks and the end-to-end run above.Validation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
git logoutput it can consume, so it works across arbitrarily long tag spans.Release Productioncan publish again. Release-note content is unchanged — same records, same ordering, same rendering.Parity Contract
parseGitLogkeeps its exported signature and output; it and the streaming path share oneparseCommitRecord, so records parse identically. Record ordering still comes fromgit log --reverse.streamGitRecordsrejects on a non-zero git exit and includes git's stderr, so a bad ref still fails loudly rather than yielding empty notes — the bufferedexecFileSyncpath also threw on non-zero exit.Duplicate / Superseded PR Handling
Summary by CodeRabbit
Bug Fixes
Tests
Chores