Skip to content

ci: keep the Linux Nix store on a Blacksmith sticky disk - #1882

Merged
ryoppippi merged 4 commits into
mainfrom
ci/linux-nix-sticky-disk
Oct 14, 2026
Merged

ryoppippi merged 4 commits into
mainfrom
ci/linux-nix-sticky-disk

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Move the Nix store on Blacksmith Linux runners from cache-nix-action to a Blacksmith sticky disk mounted at /nix, and stop release builds from persisting the store. macOS and GitHub-hosted runners keep using cache-nix-action; reducing its write volume for macOS is left for a follow-up PR.

The goal is a faster release workflow. After #1878 the release took ~8.4 minutes (run 38470996208), and its critical path was build-linux-arm64 (284s) → npm (120s) → release (49s). The arm64 leg spent ~90s fetching the toolchain and ~50s rebuilding the Crane deps because it never had a warm cache, then ~41s saving a cache entry that would be evicted again before the next release.

Why the current cache misses

  • Across ~25 CI pushes to main (2026-09-25 to 2026-10-13), the native build legs restored a Nix store entry in only 3–6 of ~19 runs each, on macOS and Linux alike. A hit roughly halves the build step (linux-arm64: 82s vs 213s; mac-arm64: 64s vs 147s).
  • Blacksmith's Actions cache allows 25 GB per repository per week with LRU eviction (docs).
  • refactor(ci): unify all Nix caching on one job+matrix-scoped action #1296 appended github.sha to every key on the assumption that the backend was effectively unlimited, so every job writes a fresh 1.3–3 GB entry on every run:
    • ~12 GB per push to main
    • ~6 GB per PR run
    • up to 2.5 GB per hourly update-pricing run
  • That adds up to several hundred GB per day against a 25 GB weekly budget. Entries are evicted within minutes, so restores mostly miss. Even on a hit, the restore takes ~60s and the post-job save another ~40s.
  • Releases are infrequent, so their own nix-tagpr-* entries never survive until the next release.

Why sticky disks, and why they failed before

A sticky disk is an ext4 block device. Each job mounts a copy-on-write clone of the last committed snapshot, so the whole store is there at mount time, with no tarball restore, no save step, and nothing counted against the cache quota (docs, action). The stickydisk README has an official example that mounts /nix before running nix-quick-install-action, which is the setup used here.

#1296 removed an earlier sticky-disk setup. This PR addresses each of the problems it ran into:

Problem then Now
The disk was keyed per github.job, so no job shared a warm store with another. The key is <repo>-nix-<arch>-<job>-<matrix leg>, without a workflow name. release.yaml's build-native-packages therefore mounts the disk that ci.yaml commits on every push to main. autofix runs only on pull requests, so it would never commit; it borrows preflight's disk through the new disk-job input, since both build the treefmt closure.
The trim dropped Crane's cargoArtifacts because nothing rooted them (#1274 reverted, #1279). The Linux build roots them at /nix/var/nix/gcroots/ci/ccusage-cargo-artifacts. That path is a real GC root and persists with the disk, unlike the previous $RUNNER_TEMP out-link. keep-derivations and keep-outputs make GC keep their build-time closure (toolchain, vendored sources) as well.
The weekly sticky-disk GC workflow mounted its own disk and never touched the real ones (#1295). The job that commits does the trimming: once the disk is more than half full, it runs nix-collect-garbage --delete-old before adding to the store. Jobs without persistent roots go cold once per trim.

Trust model

Note

Blacksmith's "Sticky Disks: branch protection" is enabled (dashboard → Settings → Caching). Blacksmith only commits snapshots from default-branch push, schedule, and workflow_dispatch runs, and discards changes from every other run on the server side. Verified on this PR's CI: the mount step logs Sticky disk changes will not be committed for this job (branch protection: event "pull_request" is not a trusted trigger).

setup-nix-cache also sets commit: false unless the run is on main and is not a pull_request or pull_request_target event. That gate is computed from workflow and action content, which a fork PR controls: a pull_request run uses the PR's version of the workflow files, so a fork could add its own stickydisk step with commit: true. The in-action gate only keeps same-repository PR runs and release builds from committing by accident. The server-side setting is the actual trust boundary for the store that main and releases build from (raised by Cubic and pullfrog in review).

The cache-nix-action path keeps its existing behaviour, now also gated by save. Actions cache entries from PRs are scoped to the PR ref, so they never reach main.

What Changed

  • .github/actions/setup-nix-cache
    • Selects the backend: a sticky disk on Blacksmith Linux, detected through the BLACKSMITH_STICKY_DISK_GRPC_PORT variable that stickydisk itself uses; cache-nix-action everywhere else.
    • Mounts useblacksmith/stickydisk v1.7.2 (pinned by SHA) at /nix before installing Nix.
    • Adds the keep-derivations and keep-outputs settings.
    • Adds the disk-job and save inputs.
    • Logs df -h /nix on every sticky-disk run and trims committing runs at 50% usage.
    • Computes the long hashFiles list once, in its own step.
  • .github/actions/build-linux-native-package: persistent GC root for the Crane artifacts, plus a save-nix-cache input.
  • .github/actions/build-macos-nix-native-package: save-nix-cache input.
  • .github/workflows/release.yaml
    • Native builds pass save-nix-cache: false. On Linux a commit would only race CI's equivalent commits; on macOS the ~40s save was pure overhead because the entry never survives until the next release.
    • Native build jobs check out the tag shallowly. Nothing in the build path reads git history, and the full clone cost up to 41s on build-windows-x64.
  • .github/workflows/autofix.yaml: disk-job: preflight.
  • .github/workflows/ci.yaml: both ccusage-perf-comment legs use disk-job: test. They run only on pull requests and would otherwise mount a freshly formatted disk every time. The per-runtime cache-scope is dropped because read-only mounts never save.

Expected impact

  • Linux CI jobs (tirith-scan, preflight, test, the Linux native builds, npm-publish-dry-run-and-upload-pkg-pr-now, update-pricing, pullfrog, release, plus autofix and the perf comments through their donor disks): no ~60s restore and no ~35–40s save, and the deps stay warm instead of being rebuilt on most runs.
  • Release: the Linux builds start warm from CI's disk; the macOS builds and the Linux builds skip the save; the shallow checkout trims the Windows legs. The macOS legs (~160–200s) should become the slowest builds.
  • macOS: with Linux no longer writing roughly 10 GB per push to the Actions cache, macOS entries should survive eviction more often. A follow-up will reduce macOS's own write volume.

Not confirmed

  • The sticky disk's capacity is not documented. CI on this PR shows 590 GB (/dev/vdb 590G 111M 590G 1% /nix), against 2–5 GB used per job. The trim threshold is relative (50%) and every run logs df -h /nix; in practice the trim is a safety net. A scheduled GC was considered and rejected: a GC-only job sees only persistent roots, so it would wipe every job's tool closures on every schedule, and it would have to enumerate every disk key, which is the failure mode of chore(ci): remove the dead weekly sticky-disk GC workflow #1295.
  • Which snapshot wins when two runs commit the same key concurrently is not documented. Either outcome is a valid store for a cache.
  • CI on this PR passed. Every Linux job mounted its sticky disk, installed Nix on top of it, and logged commit: false. These runs only exercise the read-only path: pull requests never commit, and the new keys start freshly formatted. The warm path can only be observed on main after merge.

Testing

  • nix build .#checks.aarch64-darwin.treefmt passes. It covers actionlint and zizmor with the repository config.
  • nix run nixpkgs#actionlint on the changed workflows reports only the existing parallel: steps in ci.yaml and update-pricing.yaml.
  • Commit and pre-push hooks (treefmt, gitleaks, commit scope) pass.
  • cache-nix-action ignores the blank lines an unset input leaves behind (getInputAsArray filters empty strings). nix-quick-install-action v35 keeps an existing writable /nix: it extracts with --skip-old-files and registers its own paths with --load-db.

The native build jobs checked out the release tag with fetch-depth: 0,
but nothing in the build path reads git history: CI builds the same
outputs from a default shallow checkout. The full clone cost up to 41s
on build-windows-x64 in run 38470996208, which finished within 20s of
the slowest leg.
The cache-nix-action store cache misses most of the time. Across ~25
CI pushes to main between 2026-09-25 and 2026-10-13, the native build
legs restored an entry in only 3-6 of ~19 runs each (macOS and Linux
alike), and a hit roughly halves the build step (linux-arm64: 82s vs
213s). Blacksmith's Actions cache allows 25 GB per repository per week
with LRU eviction, while the per-commit keys introduced in #1296 write
a fresh 1.3-3 GB entry for every job on every run: ~12 GB per push to
main, ~6 GB per PR run, up to 2.5 GB per hourly update-pricing run.
#1296 assumed the backend was effectively unlimited; it is not. Even
on a hit, restoring takes ~60s and the post-job save another ~40s.

On Blacksmith Linux runners, mount /nix from a sticky disk instead. A
job gets a copy-on-write clone of the last committed snapshot, so the
whole store is present at mount time with no restore or save step and
nothing counted against the cache quota. macOS and GitHub-hosted
runners have no sticky disks and keep using cache-nix-action.

#1296 dropped an earlier sticky-disk setup. This one avoids its
problems:

- Keys are per job and matrix leg but carry no workflow name, so
  release.yaml's build-native-packages mounts the disk ci.yaml keeps
  warm on every push to main instead of starting cold. autofix, which
  only runs on pull requests and so would never commit, borrows
  preflight's disk via the new disk-job input; both build the treefmt
  closure.
- The old trim dropped Crane's cargoArtifacts because nothing rooted
  them (#1274, #1279). The Linux build now roots them inside
  /nix/var/nix/gcroots, which persists with the disk, and
  keep-derivations/keep-outputs make GC keep their build-time closure
  (toolchain, vendored sources) too.
- The weekly GC workflow (#1295) mounted its own disk and never touched
  the real ones. Trimming now happens in the job that commits: once the
  disk is over half full, `nix-collect-garbage --delete-old` runs before
  the job adds to the store. Jobs without persistent roots go cold once
  per trim.

Only runs on main that are not pull_request(_target) events commit,
whatever `save` says, so pull request code (including forks) can read
main's store but never write the store that main and releases build
from. cache-nix-action keeps its existing behaviour, now also gated by
`save`.

Not confirmed from Blacksmith's documentation: the disk's capacity
(hence the relative threshold and the `df` log on every run) and which
snapshot wins when two runs commit the same key concurrently. Either
outcome is a valid store for a cache.
Release builds now mount the Linux sticky disk that CI commits on every
push to main. Committing it from the release as well would only race
those commits with an equivalent store.

On macOS, the release's own cache-nix-action entries never survive to
the next release under Blacksmith's cache eviction, yet each save took
~40s (build-mac-arm64 and build-mac-x64 in run 38470996208) before the
job could finish and let the npm job start.

Add a save-nix-cache input to both native build actions, forwarded to
setup-nix-cache, skip rooting the Crane artifacts when nothing will be
saved, and pass save-nix-cache: false from release.yaml.
@ryoppippi

Copy link
Copy Markdown
Member Author

@coderabbitai review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review
@cubic-dev-ai review

@ryoppippi I have started the AI code review. It will take a few minutes to complete.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 70a1435e-34ba-4d52-b978-ef21b4634370
📥 Commits

Reviewing files that changed from the base of the PR and between 868991e and dd2a2cc.

📒 Files selected for processing: 1
  • .github/workflows/ci.yaml

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📝 Walkthrough

The Nix cache action now selects a Blacksmith sticky /nix disk on eligible Linux runners and uses cache-nix-action otherwise. Native-package actions expose cache-save controls. Workflows configure disk reuse and cache persistence for autofix, CI, and release jobs.

Changes: Nix cache setup and use

Layer / File(s) Summary
Backend selection and cache behavior
.github/actions/setup-nix-cache/action.yaml
The action selects a sticky disk on eligible Linux runners and uses cache-nix-action otherwise. It hashes dependencies for fallback cache keys and conditionally trims sticky-disk usage.
Native-package cache controls
.github/actions/build-linux-native-package/action.yaml, .github/actions/build-macos-nix-native-package/action.yaml
Both actions expose save-nix-cache and pass it to setup-nix-cache. The Linux action roots its Crane artifact only when saving is enabled.
Workflow cache settings
.github/workflows/autofix.yaml, .github/workflows/ci.yaml, .github/workflows/release.yaml
The autofix workflow selects preflight for its sticky disk. The CI workflow selects the test job’s disk. The release workflow disables cache saving for both native-package actions and removes the full-history checkout setting.

Priority: ➖ Normal

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

Change: Other

Merge Risk: ⚪ Minimal · up to dd2a2

A missing sticky-disk snapshot may make a pull-request run use a cold store, but no material build or release regression is established. The native release inputs remain available from the checked-out tag.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: keeping the Linux Nix store on a Blacksmith sticky disk for CI.
✨ Finishing Touches: 💡 1
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ccusage-guide dd2a2cc Commit Preview URL

Branch Preview URL
Oct 13 2026, 11:18 PM

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@pkg-pr-new

pkg-pr-new Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

ccusage

npx https://pkg.pr.new/ccusage@1882

@ccusage/ccusage-darwin-arm64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-arm64@1882

@ccusage/ccusage-darwin-x64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-x64@1882

@ccusage/ccusage-linux-arm64

npx https://pkg.pr.new/@ccusage/ccusage-linux-arm64@1882

@ccusage/ccusage-linux-x64

npx https://pkg.pr.new/@ccusage/ccusage-linux-x64@1882

@ccusage/ccusage-win32-x64

npx https://pkg.pr.new/@ccusage/ccusage-win32-x64@1882

commit: dd2a2cc

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: 868991e3d1d8
Base SHA: 57afdc255fcb

This compares the PR package against the configured base package on the same CI runner.

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 378.6ms 2.66 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 352.3ms 2.86 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 135.1ms 7.45 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 118.3ms 8.51 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 44.7ms 37.4ms 1.19x 57.00 MiB 57.00 MiB 1.00x 0.03 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 40.8ms 37.6ms 1.09x 57.00 MiB 57.00 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 37.0ms 42.1ms 0.88x 57.00 MiB 57.25 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 35.5ms 35.9ms 0.99x 57.25 MiB 57.00 MiB 1.00x 0.02 MiB/s 0.02 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 369.2ms 363.1ms 1.02x 921.10 MiB 919.09 MiB 1.00x 2.73 GiB/s 2.77 GiB/s
codex --offline --json 1.01 GiB 147.9ms 136.5ms 1.08x 517.43 MiB 495.43 MiB 0.96x 6.81 GiB/s 7.38 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4796.28 KiB 4796.28 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: 868991e3d1d8
Base SHA: 57afdc255fcb

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 342.2ms 2.94 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 307.2ms 3.28 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 150.2ms 6.70 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 120.4ms 8.37 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 48.1ms 21.6ms 2.22x 57.00 MiB 22.96 MiB 0.40x 0.03 MiB/s 0.07 MiB/s
claude session --offline --json 0.00 MiB 39.9ms 14.1ms 2.83x 57.25 MiB 22.96 MiB 0.40x 0.04 MiB/s 0.11 MiB/s
codex daily --offline --json 0.00 MiB 45.4ms 15.6ms 2.92x 57.75 MiB 26.96 MiB 0.47x 0.02 MiB/s 0.06 MiB/s
codex session --offline --json 0.00 MiB 42.7ms 14.1ms 3.03x 56.75 MiB 26.96 MiB 0.48x 0.02 MiB/s 0.06 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 344.4ms 335.6ms 1.03x 891.08 MiB 907.09 MiB 1.02x 2.92 GiB/s 3.00 GiB/s
codex --offline --json 1.01 GiB 148.7ms 124.8ms 1.19x 493.43 MiB 471.43 MiB 0.96x 6.77 GiB/s 8.06 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4796.28 KiB 4796.28 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

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.

@pullfrog pullfrog 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.

Important

Confirm the sticky-disk trust boundary and two persistence gaps before merging.

Reviewed changes

  • Sticky-disk backend on Blacksmith Linux: setup-nix-cache mounts a useblacksmith/stickydisk volume at /nix when the Blacksmith agent variable is present, falling back to cache-nix-action on macOS and GitHub-hosted runners.
  • Trust-gated commits: disks commit only when save is true and the run is on main outside pull_request(_target) events.
  • Warmer Crane closures: Linux builds root artifacts under /nix/var/nix/gcroots, enable keep-derivations/keep-outputs, and committing runs trim past 50% capacity.
  • Release slim-down: native builds pass save-nix-cache: false and check out the tag shallowly; autofix borrows preflight's disk via disk-job.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using muse-spark-1.3-contributor (free via Pullfrog for OSS) | 𝕏

Comment thread .github/workflows/release.yaml Resolved
ccusage-perf-comment only runs on pull requests, and pull requests
never commit a sticky disk, so its own per-leg keys would stay freshly
formatted on every run. Mount the disk test commits on every push to
main instead: it installs `just` and resolves the same flake inputs.

Both legs mount it read-only, so the per-runtime cache-scope that kept
their saves apart is no longer needed.

Reported by pullfrog in review.
@ryoppippi

Copy link
Copy Markdown
Member Author

@coderabbitai review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review
@cubic-dev-ai review

@ryoppippi I have started the AI code review. It will take a few minutes to complete.

@pullfrog pullfrog 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.

ℹ️ Perf-leg fix looks good — one prior confirmation still open.

Reviewed changes since the prior pullfrog review (868991e3) — one new commit (dd2a2cc4) addressing the cold perf-comment disk feedback.

  • Pointed perf legs at test's sticky disk: both ccusage-perf-comment legs now pass disk-job: test and drop the per-runtime cache-scope, so their key matches the donor test key on the same blacksmith-32vcpu-ubuntu-2404 runner.
  • Kept the share read-only: the perf job is pull_request-only, so its mounts stay commit: false while test keeps committing on main pushes with the just closure the perf legs need.

No new issues in this delta. Merging is still gated on the open thread: please confirm Blacksmith dashboard Branch Protection for sticky disks is enabled, since the in-action commit gate alone cannot stop a fork PR from mounting the shared disk with its own commit: true.

Pullfrog  | Fix it ➔ | View workflow run | Using muse-spark-1.3-contributor (free via Pullfrog for OSS) | 𝕏

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: dd2a2cc482ac
Base SHA: 57afdc255fcb

This compares the PR package against the configured base package on the same CI runner.

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 420.1ms 2.40 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 377.7ms 2.67 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 143.8ms 7.00 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 119.5ms 8.43 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 46.3ms 43.7ms 1.06x 57.00 MiB 57.25 MiB 1.00x 0.03 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 39.1ms 40.2ms 0.97x 57.25 MiB 57.25 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 37.6ms 36.3ms 1.04x 56.75 MiB 57.00 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 38.3ms 36.6ms 1.05x 57.00 MiB 57.25 MiB 1.00x 0.02 MiB/s 0.02 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 373.8ms 384.0ms 0.97x 941.10 MiB 935.10 MiB 0.99x 2.69 GiB/s 2.62 GiB/s
codex --offline --json 1.01 GiB 145.1ms 147.4ms 0.98x 513.44 MiB 499.43 MiB 0.97x 6.94 GiB/s 6.83 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB -0.00 KiB 1.00x
installed native package binary 4796.28 KiB 4796.28 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: dd2a2cc482ac
Base SHA: 57afdc255fcb

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 382.5ms 2.63 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 338.4ms 2.98 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 139.5ms 7.22 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 111.6ms 9.02 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 42.2ms 13.0ms 3.24x 57.00 MiB 22.96 MiB 0.40x 0.04 MiB/s 0.12 MiB/s
claude session --offline --json 0.00 MiB 37.2ms 19.3ms 1.93x 57.00 MiB 22.97 MiB 0.40x 0.04 MiB/s 0.08 MiB/s
codex daily --offline --json 0.00 MiB 36.9ms 12.1ms 3.04x 57.00 MiB 26.96 MiB 0.47x 0.02 MiB/s 0.07 MiB/s
codex session --offline --json 0.00 MiB 35.9ms 12.7ms 2.84x 57.25 MiB 26.96 MiB 0.47x 0.02 MiB/s 0.07 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 383.8ms 362.2ms 1.06x 953.10 MiB 941.09 MiB 0.99x 2.62 GiB/s 2.78 GiB/s
codex --offline --json 1.01 GiB 141.9ms 111.0ms 1.28x 497.43 MiB 489.43 MiB 0.98x 7.10 GiB/s 9.07 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB -0.00 KiB 1.00x
installed native package binary 4796.28 KiB 4796.28 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

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.

@ryoppippi
ryoppippi merged commit 6ca2776 into main Oct 14, 2026
93 of 95 checks passed
@ryoppippi
ryoppippi deleted the ci/linux-nix-sticky-disk branch October 14, 2026 11:14
ryoppippi added a commit that referenced this pull request Oct 18, 2026
… layers (#1921)

* ci(nix): drop the workflow name from the Nix cache key

release.yaml builds the macOS packages with save-nix-cache: false, so
its keys (nix-tagpr-build-native-packages-...) never had an entry to
restore: the v20.0.35 release (run 39053076844, build-mac-arm64) found
no cache, built 161 derivations including the Rust toolchain, fetched
185 paths, and spent 139s in the build step.

Key the store by job and matrix scope only, like the sticky disk key.
Release builds run on pushes to main, so they now restore the newest
entry CI saved on main, often for the very commit being released, and
still never write one. The commit sha keeps workflows that share a job
id from blocking each other's saves; only runs for the same commit can
collide, and their stores hold the same build.

The key is computed once in a step and exposed through its outputs so a
later change can reuse it outside cache-nix-action's combined step.
Existing nix-CI-* and nix-tagpr-* entries stop matching, so each cached
job restores cold once.

* ci(nix): save the macOS Nix store explicitly after rooting its closure

No CI run has saved a macOS Nix store since #1882 (6ca2776). The
build-mac-arm64 leg of every push to main from 2026-10-14T11:14Z to
2026-10-18 (24 runs) restored the same entry,
nix-CI-build-native-packages-build-mac-arm64-05c2a29d...-75d48e98,
written by the last saving run (38547180232), and its post-job "Cache
Nix store" step finished in ~100 ms without a line of output. In
cache-nix-action v7.0.2 the only silent exit from the save is
`save` not being "true": #1882 started passing
`save: ${{ inputs.save }}`, and the post step of a cache-nix-action
nested in build-macos-nix-native-package -> setup-nix-cache evidently
reads it as empty. Before #1882 it fell back to the "true" default and
saved; the single-level tagpr npm job still saves today.

So every macOS build restored a four-day-old 1.4 GB entry (~50 s, 47 s
of it gtar extraction), found that the deps hash had moved on, and
rebuilt the whole Crane chain: deps ~20 s, foundation ~9 s, adapters
~5 s, ccusage ~25 s.

setup-nix-cache gains defer-save: it only restores, and exposes the
primary key and whether this run may save. The macOS build saves with
cache-nix-action/save as an ordinary step, so nothing depends on how
post steps evaluate inputs. Saving needs a push to main as well as
save-nix-cache, matching the sticky disks: a pull request would only
spend ~40 s per leg writing an entry main cannot read, adding eviction
pressure on Blacksmith's cache.

Before saving, the build roots what the next run needs in the store's
own gcroots, like the Linux build: the Crane cacheRoot (keep-outputs
holds its toolchain and vendored sources) and the flake inputs this
build fetched, found with `nix flake archive --dry-run` so unused pins
are not downloaded. The save then collects every other path
(gc-max-store-size-macos: 0), so an entry is one build closure instead
of accumulating every commit's layers and binaries.

* build(rust): install the minimal rustup profile plus explicit components

rust-toolchain.toml named no profile, so rustup and rust-overlay used
"default", which adds rust-docs: 728 MB and 65,585 files in
rust-docs-1.97.1-aarch64-apple-darwin, roughly half the files of the
macOS build closure. Every cold build downloads and unpacks it (the
v20.0.35 release built rust-docs-1.97.1-aarch64-apple-darwin.tar.xz),
and every cache-nix-action entry tars it, a large share of the work in the ~47 s
gtar extraction on a macOS restore.

Nothing builds or reads the docs. The minimal profile keeps rustc,
cargo, and rust-std, and the existing components list still adds
clippy, llvm-tools-preview, and rustfmt; rust-overlay also stops
pulling libsecret into the Linux toolchain for non-minimal profiles.
The toolchain derivation changes, so every Nix store and the Windows
cargo cache rebuild once.

* ci: correct which entry release macOS builds restore

Release starts before CI saves the entry for the release commit, so it restores the previous main commit's store, which still matches because release commits only bump package.json files.
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.

1 participant