Skip to content

fix(release): stream git log so release notes survive long tag spans - #6031

Open
M3gA-Mind wants to merge 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/release-notes-git-log-enobufs
Open

fix(release): stream git log so release notes survive long tag spans#6031
M3gA-Mind wants to merge 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/release-notes-git-log-enobufs

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Stream git log in the release-notes generator instead of buffering it, fixing the spawnSync git ENOBUFS that has blocked Release Production since v0.63.12.
  • Apply the same fix to priorAuthorKeys, which walks the entire repo history and so was unbounded by construction, not just by release span.
  • Stop the fallback step from claiming the AI step "failed or timed out" — on the failing run both steps died on the same ENOBUFS, and that wording misdirected the investigation.
  • Arm ci-lite's scripts lane for scripts/release/*.mjs, whose node --test suite could not run on changes to the file it covers.

Problem

Run 33753636908 (branch release, sha 399968801) failed in Prepare GitHub release:

##[warning]AI release notes failed or timed out — using deterministic notes.
[release-notes] Collecting tinyhumansai/openhuman changes from v0.63.12 to v0.63.21
[release-notes] spawnSync git ENOBUFS
##[error]Process completed with exit code 1.

execFileSync buffers the child's whole stdout and throws ENOBUFS past Node's 1 MiB maxBuffer default. The offending line was in collectCommits:

const output = runGit(['log', `${from}..${to}`, '--reverse', `--format=${format}`]);

This ratchets. --from latest-release resolves to the last published release, so each release that fails to publish lengthens the next range. Measured against the real tags:

range commits git log bytes % of 1 MiB outcome
v0.63.12..v0.63.17 (2026-08-21) 2,311 394,024 38% published OK
v0.63.12..v0.63.21 (2026-09-03) 6,973 1,178,143 112% ENOBUFS

No GitHub Release has been created since v0.63.12 on 2026-08-07; v0.63.17, v0.63.20 and v0.63.21 are tagged with nothing published behind them.

Solution

collectCommits and priorAuthorKeys now stream git's stdout through spawn and 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 and priorAuthorKeys grows 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.

parseGitLog keeps its existing exported signature and behaviour; it and the streaming path now share one parseCommitRecord.

Not fixed here (filed in the issue as follow-up): collectPullRequests makes one sequential gh pr view per 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's timeout-minutes: 5 does 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

  • Release lane only — no runtime, app, or CLI surface is touched.
  • Memory profile improves: neither collector now holds the full git log output.
  • Both collectors became async; the single call site in main() (already async) awaits them.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — scripts/__tests__/generate-release-notes.test.mjs gains two tests: a chunk-boundary test for the splitter, and an end-to-end test that builds a fixture repo whose git log output exceeds 1 MiB and asserts every commit is collected. Both were revert-checked (see below).
  • N/A: Diff coverage ≥ 80% — the diff-cover gate measures Vitest + cargo-llvm-cov; scripts/** is outside both instrumented trees. The changed lines are covered by the node --test suite in the scripts lane, which this PR also arms for this directory.
  • N/A: Coverage matrix updated — release-tooling change, no feature row in docs/TEST-COVERAGE-MATRIX.md.
  • N/A: All affected feature IDs listed under ## Related — no matrix feature IDs apply.
  • No new external network dependencies introduced — the new code spawns git, which the script already invoked.
  • N/A: Manual smoke checklist updated — docs/RELEASE-MANUAL-SMOKE.md covers built artefacts; this changes how release-note text is generated, not what ships.
  • Linked issue closed via Closes #NNN — see ## Related.

How this was verified

  1. node --test scripts/__tests__/generate-release-notes.test.mjs12/12 pass (was 10 tests before this PR).
  2. Revert-check, streaming collector. Restoring the execFileSync version of collectCommits with the new test in place fails with the production error verbatim:
    ✖ commit collection survives a git log larger than the 1 MiB spawn buffer
      Error: spawnSync git ENOBUFS
    
    The test asserts its fixture actually exceeds 1 MiB before collecting, so it cannot pass vacuously on a small repo.
  3. Revert-check, splitter. Replacing the incremental splitter with a naive per-chunk chunk.split(separator) fails the boundary test (actual drops records when fed one byte at a time).
  4. End-to-end against the real failing rangegenerate-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.
  5. Both edited workflow files re-parsed as YAML.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/release-notes-git-log-enobufs
  • Commit SHA: b130e2e24

Validation Run

  • N/A: pnpm --filter openhuman-app format:check — no files under app/ changed; format/lint in this repo are scoped to openhuman-app.
  • N/A: pnpm typecheck — no TypeScript changed; the edited files are .mjs, YAML, and a node --test suite.
  • Focused tests: node --test scripts/__tests__/generate-release-notes.test.mjs (12/12), plus the two revert-checks and the end-to-end run above.
  • N/A: Rust fmt/check — no Rust changed.
  • N/A: Tauri fmt/check — no Tauri code changed.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: the release-notes generator no longer has a fixed cap on how much git log output it can consume, so it works across arbitrarily long tag spans.
  • User-visible effect: Release Production can publish again. Release-note content is unchanged — same records, same ordering, same rendering.

Parity Contract

  • Legacy behavior preserved: parseGitLog keeps its exported signature and output; it and the streaming path share one parseCommitRecord, so records parse identically. Record ordering still comes from git log --reverse.
  • Guard/fallback/dispatch parity checks: streamGitRecords rejects on a non-zero git exit and includes git's stderr, so a bad ref still fails loudly rather than yielding empty notes — the buffered execFileSync path also threw on non-zero exit.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

  • Bug Fixes

    • Improved release-note generation for repositories with large commit histories, preventing failures caused by oversized Git output.
    • Release workflow warnings now accurately reflect the AI step’s outcome and direct users to its logs for investigation.
  • Tests

    • Added coverage for large commit histories and records split across streamed data chunks.
  • Chores

    • Updates to release-note generator modules now trigger the appropriate self-test workflow.

@M3gA-Mind
M3gA-Mind requested a review from a team September 4, 2026 13:26
@tinysweeper

tinysweeper Bot commented Sep 4, 2026

Copy link
Copy Markdown

How this change flows

2 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
Loading

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.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Sep 4, 2026
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 20f8809b-e00a-4e54-b8fd-77de2662cd5f

📥 Commits

Reviewing files that changed from the base of the PR and between b130e2e and 2abc5d7.

📒 Files selected for processing (1)
  • .github/workflows/ci-lite.yml

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The release-note generator now streams git log records instead of buffering complete output. Commit collection is asynchronous. Tests cover large histories and chunk boundaries. CI watches release script changes, and fallback warnings report the AI step outcome accurately.

Changes

Release-note generation

Layer / File(s) Summary
Streaming Git records
scripts/release/generate-release-notes.mjs
The generator splits streamed Git output into records and parses commit fields incrementally.
Async commit collection
scripts/release/generate-release-notes.mjs
collectCommits and priorAuthorKeys use the streaming path. main awaits both functions.
Regression coverage and workflow wiring
scripts/__tests__/generate-release-notes.test.mjs, .github/workflows/ci-lite.yml, .github/workflows/release-production.yml
Tests cover chunk boundaries and Git output larger than 1 MiB. CI includes release scripts, and the fallback warning points to the AI step log without asserting a cause.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 2abc5

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
Loading

Suggested reviewers: senamakel

Poem

A rabbit checks the stream,
Git records hop through narrow gaps,
Chunked lines join as one,
Tests guard the long trail,
Release notes reach the moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: streaming Git log output to prevent release-note failures for long tag ranges.
Linked Issues check ✅ Passed The changes address issue [#6030] by streaming both bounded and unbounded Git history reads, adding regression tests for chunk boundaries and outputs above 1 MiB, correcting fallback warning text, and…
Out of Scope Changes check ✅ Passed All changes support issue [#6030] objectives. No unrelated code or behavior changes are present.
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@M3gA-Mind
M3gA-Mind force-pushed the fix/release-notes-git-log-enobufs branch from b130e2e to 2abc5d7 Compare September 7, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release Production cannot publish: release-notes generator dies with spawnSync git ENOBUFS on long tag spans

1 participant