Skip to content

ci: refold the metrics push instead of rebasing it (#126) - #127

Merged
mobileskyfi merged 3 commits into
mainfrom
fix/126-refold-metrics-push
Aug 2, 2026
Merged

ci: refold the metrics push instead of rebasing it (#126)#127
mobileskyfi merged 3 commits into
mainfrom
fix/126-refold-metrics-push

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #126. Also carries B8a's durable findings into ci.instructions.md, as asked.

The bug

The ci-data push retry resolved a race with git pull --rebase … || true. That is safe only for files that cannot conflict. runs/*.ndjson is unique per leg and qualifies — but tested-versions.json and attempted-legs.json are shared rollups every concurrent run rewrites, so runs landing together conflict as a matter of course.

When they did, the rebase stopped with unmerged files and || true swallowed it. Every later attempt then died before it started:

push attempt 1 failed — rebasing onto remote ci-data
CONFLICT (content): Merge conflict in attempted-legs.json
push attempt 2 failed — rebasing onto remote ci-data
error: Pulling is not possible because you have unmerged files.
fatal: Exiting because of an unresolved conflict.

All four attempts burned in 1.1 s — the loop had no backoff, and after the first conflict nothing could ever succeed. Run 30760428239 (B8a of #110) completed a leg cleanly and still lost its metrics: the log shows create mode 100644 runs/30760428239-macos-x86-stable.ndjson, and the file is not on the branch.

attempted-legs.json is B5's ledger — the file that records which legs never finished. A sweep where several legs die together is both the likeliest trigger and the case where the ledger matters most.

The fix

Recompute instead of merge. On a failed push: abort any conflicted rebase/merge unconditionally (never assume the tree is clean on the way into a retry), reset to the freshly fetched head, re-run the fold, re-commit, retry — with real backoff.

This is correct because both writers are read-modify-write keyed by identity: ci-leg-ledger keys attempted-legs.json by run id, ci-metrics folds tested-versions.json by version/platform. Re-applying one run's contribution on top of the newest tree yields the union, with no textual merge anywhere.

That property was load-bearing and untested

If either writer ever became a wholesale replace, this retry would start silently destroying the other run's data — precisely the failure the ledger exists to prevent. So it is now extracted as foldLedgerInto and pinned:

  • another run's entry survives a fold;
  • folding the same run twice is idempotent (a retry cannot duplicate or drift);
  • a later fold of the same run id replaces only that run's verdict;
  • the input map is not mutated;
  • keys come out sorted, so a merge stays a byte-stable diff.

ci.instructions.md now states the rule for the future: any new shared file added to this push must fold the same way.

Verification

Reproduced locally rather than reasoned about — a two-clone race harness against a bare origin, with a fold that mirrors ci-leg-ledger's read-modify-write.

Old loop — reproduces the CI log exactly, and the loser's data is gone:

push attempt 1 failed — rebasing onto remote ci-data
CONFLICT (content): Merge conflict in attempted-legs.json
push attempt 2 failed — rebasing onto remote ci-data
error: Pulling is not possible because you have unmerged files.
push attempt 3 failed — rebasing onto remote ci-data
error: Pulling is not possible because you have unmerged files.
::warning::could not push metrics after 4 attempts
elapsed: 1s
attempted-legs.json: {"run-BBB":{...}}          ← run-AAA lost
runs/: run-BBB.ndjson                            ← run-AAA lost

New loop — recovers on the next attempt, both runs intact:

push attempt 1 failed — refolding onto the current remote head
attempted-legs.json: {"run-AAA":{...},"run-BBB":{...}}
runs/: run-AAA.ndjson run-BBB.ndjson

Also: actionlint clean (the two SC2016 infos it reports are pre-existing, at line 586, not this step), the extracted step passes bash -n and shellcheck -S warning standalone, and bun test test/unit/ is 950 pass / 0 fail with tsc --noEmit and Biome clean.

Included per request: B8a durable knowledge

Two additions to ci.instructions.md, both from the B8a report on #76:

  • Hosted macos-15-intel is 4 CPUs / 14336 MiB, measured across four runners. That is half the cores and 22% of the RAM of the maintainer's Intel Mac — and it is nevertheless faster per file, so the size gap is not a reason to expect hosted legs to be slower. Boot there is 29.8–40.4 s under HVF over 29 boots. This closes the "the gap is uncharacterized" caveat CI: macos-x86 full suite wedges at position 10 in provisioning.test.ts — host-level runner loss, mechanism unexplained (HVF) #76 has carried since B7.
  • Per-file macos-x86 costs, with an explicit warning not to fold them into OBSERVED_MAX_S: they are bounded-group runs, never a full suite, and the caps stay deliberately generous on the one platform under investigation. They are recorded because they answer a different question — the suite's own cost is ~30 min, not the 62–65 min completed_at reports.

Scope

The filter=all check-run gotcha B8a also hit needed no change — ci-leg-ledger.ts:300 already documents and handles it. No CHANGELOG entry: CI-internal, nothing user-facing, matching B4/B5.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved CI metrics publishing so transient push conflicts retry against the latest data and are less likely to fail workflows.
    • Preserved existing run metrics when updating individual run records, improving consistency and preventing unintended overwrites.
  • Documentation

    • Added macOS Intel performance measurements, resource details, virtual machine boot timings, and guidance for interpreting watchdog limits.
    • Documented the updated metrics retry behavior and best-effort handling.

A `ci-data` push race was resolved with `git pull --rebase … || true`. That
works only for files that cannot conflict. `runs/*.ndjson` is unique per leg
and qualifies; `tested-versions.json` and `attempted-legs.json` are shared
rollups every concurrent run rewrites, and they conflict as a matter of course.

When they did, the rebase stopped with unmerged files and `|| true` swallowed
it, so every later attempt died on "Pulling is not possible because you have
unmerged files" — all four attempts burned in 1.1 s with no backoff. Run
30760428239 (B8a of #110) completed a leg cleanly and still lost its metrics
that way: the ndjson was created and never landed.

Resolve the race by recomputing instead of merging. On a failed push, abort any
conflicted rebase/merge unconditionally, reset the worktree to the freshly
fetched head, re-run the fold, re-commit and retry, with real backoff between
attempts. Both writers are read-modify-write keyed by identity, so re-applying
one run's contribution on top of the newest tree yields the union with no
textual merge at all.

That property was load-bearing and untested. Extract it as `foldLedgerInto`
and pin it with anchor tests: another run's entry survives, a repeat fold is
idempotent, the input map is not mutated, and keys stay sorted. A writer that
replaced rather than merged would turn this retry into silent data loss — the
exact failure the ledger exists to prevent — so the doc says any future shared
file added to this push must fold the same way.

Verified with a local two-clone race harness: the old loop reproduces the CI
log exactly (conflict, then two "unmerged files" failures, ~1 s, loser's data
absent from origin); the new loop lands both runs' keys and both ndjson files.

Also records B8a's durable findings in ci.instructions.md: hosted
macos-15-intel is 4 CPUs / 14336 MiB (measured across four runners) and is
faster per file than the 8-core/64 GiB laptop, plus the per-file macos-x86
costs — with an explicit note NOT to fold them into OBSERVED_MAX_S, since they
are bounded-group runs and the caps stay deliberately generous while #76 is open.

No CHANGELOG entry — CI-internal, nothing user-facing, same as B4/B5.

Refs #110, #124, #76.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 2, 2026 18:38
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@mobileskyfi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d5537496-4843-47f6-a1d8-a4a9c39fc046

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9c341 and c219782.

📒 Files selected for processing (3)
  • .github/workflows/integration.yml
  • project-words.txt
  • scripts/ci-leg-ledger.ts
📝 Walkthrough

Walkthrough

The aggregate workflow now handles shared ci-data conflicts by resetting to the latest branch head, refolding metrics and ledger data, and retrying pushes with backoff. A pure foldLedgerInto helper and tests support deterministic ledger merging. CI documentation records the retry behavior and macOS Intel measurements.

Changes

CI data refold retries

Layer / File(s) Summary
Pure ledger merge contract
scripts/ci-leg-ledger.ts, test/unit/ci-leg-ledger.test.ts
Added foldLedgerInto to replace one run entry while preserving other entries and sorted keys. Tests cover replacement, idempotence, immutability, and ordering.
Refold-based aggregate retries
.github/workflows/integration.yml
The aggregate job aborts stale state, resets to the latest ci-data head, recomputes shared rollups, retries up to four times with backoff, and reports unsuccessful pushes as warnings.
CI measurement and retry documentation
.github/instructions/ci.instructions.md
Documented macOS Intel measurements, bounded timing guidance, host capacity, HVF boot timing, and reset-and-refold retries.

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

Sequence Diagram(s)

sequenceDiagram
  participant AggregateJob
  participant CiDataBranch
  participant FoldLedgerInto
  AggregateJob->>CiDataBranch: push metrics and ledger rollups
  CiDataBranch-->>AggregateJob: report push conflict
  AggregateJob->>CiDataBranch: fetch and reset to latest head
  AggregateJob->>FoldLedgerInto: refold current run ledger
  FoldLedgerInto-->>AggregateJob: return sorted merged ledger
  AggregateJob->>CiDataBranch: retry push with backoff
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 CI change: refolding metrics on retry instead of rebasing, and references issue #126.
Linked Issues check ✅ Passed The implementation addresses [#126] by aborting stale state, resetting to the latest ci-data head, refolding shared files, and retrying with backoff.
Out of Scope Changes check ✅ Passed The workflow, ledger helper, tests, and documentation changes directly support the retry fix and its stated CI measurement objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/126-refold-metrics-push

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Pull request overview

This PR fixes a CI metrics publishing race in the ci-data branch update path by switching the retry strategy from “rebase and retry” to “reset to remote head, refold, recommit, retry”, ensuring shared rollup files can’t get stuck in a conflicted rebase state. It also adds unit tests that pin the “fold-by-identity” behavior the new retry strategy relies on, and updates CI documentation with durable findings from B8a.

Changes:

  • Replace the ci-data push retry logic in integration.yml with a refold-based retry loop (abort rebase/merge, hard reset, rerun folds, backoff, retry push).
  • Extract foldLedgerInto() in scripts/ci-leg-ledger.ts and add anchor tests validating merge/idempotency/sorted-key behavior.
  • Update ci.instructions.md with macOS Intel runner resource facts and explicit guidance about the refold retry invariants.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/unit/ci-leg-ledger.test.ts Adds anchor tests for foldLedgerInto() to guarantee safe refolding semantics under concurrent pushes.
scripts/ci-leg-ledger.ts Extracts foldLedgerInto() and uses it for writing attempted-legs.json deterministically.
.github/workflows/integration.yml Reworks the ci-data push retry mechanism to refold on top of the latest remote head with backoff.
.github/instructions/ci.instructions.md Documents the new refold retry rule and carries forward B8a’s durable platform findings.

Comment thread .github/workflows/integration.yml Outdated
Comment thread scripts/ci-leg-ledger.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/integration.yml:
- Around line 1180-1230: Update refold() to track failures from both
ci-metrics.ts aggregate and ci-leg-ledger.ts build, returning nonzero for any
failure; in the retry loop, do not stage or commit partial state when refold
fails, and continue/retry with the existing warning behavior. Update
stage_and_commit() and both callers to distinguish “nothing staged” from a git
commit failure, surfacing unexpected commit failures instead of treating them as
successful no-op exits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6765ef83-0bb1-4145-94e6-9ec0283f82c9

📥 Commits

Reviewing files that changed from the base of the PR and between fe00d49 and 8d9c341.

📒 Files selected for processing (4)
  • .github/instructions/ci.instructions.md
  • .github/workflows/integration.yml
  • scripts/ci-leg-ledger.ts
  • test/unit/ci-leg-ledger.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: Unit Tests & Coverage
  • GitHub Check: Unit Tests (windows-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In Bun-based TypeScript code, use Bun.spawn(), Bun.write(), Bun.sleep(), bun:test, and ESM imports with .ts extensions.

Files:

  • scripts/ci-leg-ledger.ts
  • test/unit/ci-leg-ledger.test.ts
**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.ts: Use Bun APIs and tooling rather than Node.js equivalents: Bun.spawn(), Bun.write(), Bun.sleep(), bun test, and bun:test. Use ESM with .ts extensions in imports; do not use CommonJS.
For ARM64 virt machines, never use if=virtio for drives; use an explicit -device virtio-blk-pci,drive=drive0 configuration.
When using HVF acceleration, use -cpu host, not cortex-a710.
For arm64 guests on macOS, automatically select TCG with -cpu cortex-a710; HVF cannot run the CHR image's 32-bit ARM userspace on Apple Silicon. --accel and QUICKCHR_ACCEL must override this selection for testing.
UEFI pflash code and vars units must be identical in size.
QGA is x86-only; do not assume the guest agent starts for arm64 CHR.
Use tabs for indentation.
Do not add unnecessary comments to obvious code.
Errors must be thrown as QuickCHRError(code, message, installHint?).
Preserve the documented public API types and behavior: QuickCHR.start(opts) returns ChrInstance; ChrInstance provides stop(), remove(), rest(), monitor(), serial(), and qga(); and MachineState represents persisted machine.json state.

Files:

  • scripts/ci-leg-ledger.ts
  • test/unit/ci-leg-ledger.test.ts
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not turn a red integration test green by broadening timeouts, skipping it, or platform-gating it before reproducing and root-causing the failure.

Files:

  • test/unit/ci-leg-ledger.test.ts
test/unit/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Unit tests must be fast and must not require QEMU.

Files:

  • test/unit/ci-leg-ledger.test.ts
🧠 Learnings (4)
📚 Learning: 2026-07-31T11:56:38.870Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: scripts/ci-cache-key.ts:110-110
Timestamp: 2026-07-31T11:56:38.870Z
Learning: In Bun CI scripts under scripts/, use a plain Error for validation failures when the script catches the error and renders it as a GitHub ::error:: annotation. QuickCHRError is intended for programmatic library callers and is not required for this CLI-only error path.

Applied to files:

  • scripts/ci-leg-ledger.ts
📚 Learning: 2026-07-31T11:56:40.455Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: scripts/ci-cache-key.ts:68-68
Timestamp: 2026-07-31T11:56:40.455Z
Learning: In Bun TypeScript files, do not flag use of `node:fs.appendFileSync` when append semantics are required and `Bun.write()` cannot provide them. This applies to cases such as appending multiple entries to `$GITHUB_OUTPUT` or writing to the boot log; use append-capable file operations rather than overwriting existing content.

Applied to files:

  • scripts/ci-leg-ledger.ts
  • test/unit/ci-leg-ledger.test.ts
📚 Learning: 2026-07-28T00:12:59.340Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 101
File: .github/workflows/integration.yml:342-342
Timestamp: 2026-07-28T00:12:59.340Z
Learning: For tikoci/quickchr, review workflow changes to ensure GitHub Actions are pinned to immutable commit SHAs as a repository-wide policy. If a PR introduces SHA pinning for only part of a workflow (or only some workflows) without extending the same policy across the relevant workflow files, flag it. When adding/changing SHA pinning, also verify Dependabot is configured to update the `github-actions` ecosystem so action version bumps are managed consistently (e.g., in the repo’s Dependabot configuration), rather than doing an isolated pinning change in an unrelated PR.

Applied to files:

  • .github/workflows/integration.yml
📚 Learning: 2026-07-31T11:56:27.561Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: .github/workflows/integration.yml:188-188
Timestamp: 2026-07-31T11:56:27.561Z
Learning: For the tikoci/quickchr repository, do not request isolated SHA pinning of first-party GitHub Actions in a single workflow or pull request. Treat action pinning as repository-wide work: first configure Dependabot for the github-actions ecosystem, then mechanically pin all workflow action references, and finally decide whether to enforce zizmor in CI. Apply this guidance to workflow files while issue `#118` tracks the rollout.

Applied to files:

  • .github/workflows/integration.yml
🪛 LanguageTool
.github/instructions/ci.instructions.md

[grammar] ~285-~285: Ensure spelling is correct
Context: ...retry refolds; it never rebases** (#126). runs/*.ndjson is unique per leg an...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~293-~293: Consider using “who” when you are referring to a person instead of an object.
Context: ... it must fold the same way** — a writer that replaces rather than merges would make ...

(THAT_WHO)

🔇 Additional comments (7)
scripts/ci-leg-ledger.ts (2)

256-275: LGTM!


516-516: LGTM!

test/unit/ci-leg-ledger.test.ts (1)

10-10: LGTM!

Also applies to: 246-284

.github/workflows/integration.yml (1)

1082-1084: LGTM!

.github/instructions/ci.instructions.md (3)

153-161: LGTM!


285-298: LGTM!


411-419: LGTM!

Comment thread .github/workflows/integration.yml
mobileskyfi and others added 2 commits August 2, 2026 11:46
The push retry now describes tested-versions.json and attempted-legs.json
together, which needed a plural the word list did not carry. Matches the
existing explicit-inflection entries (normalise/normalises).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses both bot reviews on #127.

CodeRabbit (major, valid): `refold()` returned non-zero only when the metrics
aggregate failed. A failed `ci-leg-ledger build` fell through to a bare `echo`
and returned 0, and the caller discarded the status anyway with `refold || true`.
So a half-refold could stage whatever it did produce, commit it, push, and exit
0 — this run's runs/*.ndjson and tested-versions.json silently absent. That is
#126's own failure shape one level up. `refold` now reports failure for either
fold, and the loop retries instead of committing partial state.

Their suggested `continue`, taken literally, introduces a worse bug. The reset
above it discards this run's commit, so with nothing rebuilt HEAD equals the
remote and `git push` prints "Everything up-to-date" and exits 0 — a false
success hiding total loss. Demonstrated: with the loop otherwise identical and
only the guard removed, a persistent refold failure prints "PUSH SUCCEEDED on
attempt 2" with the run absent from origin. So the push is guarded by an
explicit have_commit flag that only a clean refold sets.

Copilot (valid): `fetch`/`reset` failures were swallowed by `|| true`. A stale
FETCH_HEAD would rebuild the commit against the wrong base, and the "nothing
left to record" fast-path could then wrongly claim another run carried the
data. A failed fetch now keeps the existing commit and just retries the push; a
failed reset drops have_commit and retries.

Copilot (valid): sortKeysDeep's doc comment was left stranded above the
extracted foldLedgerInto. Moved back.

Also splits stage_and_commit's "nothing staged" (1) from "commit failed" (2),
so an unexpected commit failure warns instead of exiting 0 quietly.

Verified with the race harness extended to the failure paths: persistent refold
failure ends in the loud warning with no false success and the winner's data
intact; refold failing once then succeeding recovers on attempt 3 with both
runs' keys and both ndjson files; the plain race still lands both on attempt 2.
actionlint clean, the extracted step passes bash -n and shellcheck -S warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Both reviews had real findings — thanks. All three fixed in c219782.

CodeRabbit (major) — partial refold could push partial data. Valid, fixed.
refold() returned non-zero only for a failed metrics aggregate; a failed ci-leg-ledger build fell through to a bare echo and returned 0, and the caller discarded the status anyway via refold || true. Exactly as traced: a half-refold could stage what it did produce, commit, push and exit 0 with this run's runs/*.ndjson and tested-versions.json missing — #126's own failure shape one level up. refold now reports failure for either fold and the loop retries rather than committing partial state.

But the suggested continue, taken literally, introduces a worse bug — so I did not apply that half as written. The reset --hard above it discards this run's commit. If nothing rebuilds it, HEAD equals the remote, and the next iteration's git push prints Everything up-to-date and exits 0 — a false success hiding total loss, with no warning at all.

Demonstrated rather than argued. Same loop, only the guard removed, refold failing persistently:

push attempt 1 failed — refolding onto the current remote head
::warning::refold: metrics aggregate failed on retry
::warning::refold incomplete — retrying rather than committing partial data
PUSH SUCCEEDED on attempt 2          ← no-op push reported as success
ledger: {"run-BBB":{...}}            ← run-AAA gone
runs/: run-BBB.ndjson

So the push is guarded by an explicit have_commit flag that only a clean refold sets. With it, the same scenario ends correctly:

::warning title=ci-data::could not push metrics after 4 attempts — this run's timing is only in the artifacts
ledger: {"run-BBB":{...}}            ← winner intact, no false success

And refold failing once then recovering lands everything on attempt 3: {"run-AAA":…,"run-BBB":…}, both ndjson files present.

CodeRabbit (secondary) — stage_and_commit conflated two outcomes. Valid, fixed. It now returns 1 for "nothing staged" (a clean no-op) and 2 for "the commit itself failed", and both call sites branch on them, so an unexpected commit failure warns instead of quietly exiting 0.

Copilot — fetch/reset failures swallowed by || true. Valid, fixed. A stale FETCH_HEAD would rebuild the commit against the wrong base, and the "nothing left to record" fast-path could then wrongly claim another run carried the data. A failed fetch now keeps the commit we already have and just retries the push; a failed reset clears have_commit and retries.

Copilot — stranded doc comment. Valid, fixed. sortKeysDeep's /** Deep-sort … */ was left above the extracted foldLedgerInto. Moved back.

actionlint clean, the extracted step passes bash -n and shellcheck -S warning standalone, bun run check clean, unit tests 950 pass / 0 fail.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

The loop was materially restructured in c219782 after your findings — refold now reports either fold's failure, the push is guarded by a have_commit flag so a post-reset no-op push cannot report success, fetch/reset failures branch instead of being swallowed, and stage_and_commit distinguishes "nothing staged" from "commit failed". Please re-review that control flow specifically.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline c219782
git show --format=fuller --find-renames --find-copies -- .github/workflows/integration.yml c219782
printf '\n--- workflow outline ---\n'
ast-grep outline .github/workflows/integration.yml --items all || true
printf '\n--- retry-related workflow lines ---\n'
rg -n -C 12 -e 'refold|stage_and_commit|have_commit|git (fetch|reset|push|commit)' .github/workflows/integration.yml

Length of output: 17479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- aggregate workflow context ---'
sed -n '1090,1270p' .github/workflows/integration.yml

printf '%s\n' '--- pre-change aggregate workflow context ---'
git show c219782^:.github/workflows/integration.yml | sed -n '1160,1250p'

printf '%s\n' '--- ci-metrics and ci-leg-ledger entry points ---'
ast-grep outline scripts/ci-metrics.ts --items all
ast-grep outline scripts/ci-leg-ledger.ts --items all

printf '%s\n' '--- aggregate/build command handling ---'
rg -n -C 8 -e 'aggregate|process\.exit|throw |exitCode|writeFile|tested-versions|attempted-legs' scripts/ci-metrics.ts scripts/ci-leg-ledger.ts

Length of output: 43980


@mobileskyfi: No finding in the revised retry control flow.

  • refold returns failure if either fold fails. The loop does not commit partial output.
  • have_commit blocks a post-reset no-op push.
  • A failed fetch retains the existing commit and retries its push.
  • A failed reset clears have_commit and prevents a stale-state push.
  • stage_and_commit status 1 and status 2 now take separate paths.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci-data push retry cannot survive a content conflict in attempted-legs.json — concurrent legs lose their metrics

2 participants