//! Cross-host build contexts (R636-B1 for `StepKind::BuildImage`; R560-T8 for
//! remote `StepKind::Subprocess` source trees).
//!
//! # The bug this exists for
//!
//! A remote build-image step used to hand yubaba the *composer's* paths —
//! `--local context=/yah/build/context` backed by a bind mount of the qed
//! host's camp root. That is coherent only while the builder and the composer
//! share a filesystem. Offload the step to a real build-worker and runc fails
//! at mount setup with `open /Users/leif/ss/yah: no such file or directory`,
//! because of course the Linux worker has no such directory. Every offloaded
//! catalog image build died there.
//!
//! # The shape that works
//!
//! Pack the context (plus the *compiled* Dockerfile, which lives outside it in
//! `.yah/cache/buildkit/`) into one gzipped tar, put it somewhere the worker
//! can GET, and pass BuildKit `--opt context=<url>`. The dockerfile frontend
//! fetches and unpacks it worker-side and resolves `--opt filename=` inside it,
//! so the workload references nothing host-local except the OCI output dir,
//! which is worker-local already.
//!
//! Verified against `moby/buildkit:v0.12.5-rootless` — the pinned default in
//! [`velveteen_exec`]'s `default_buildkit_image` — with the exact argv the
//! synthesis emits: `#1 [internal] load remote build context` → `#2 copy
//! /context /` → the Dockerfile's `COPY` resolving out of the tar.
//!
//! # Why the transport is a trait
//!
//! qed has no cloud credentials and should not grow any: it is the scheduler,
//! not the publisher. [`BuildContextPublisher`] is the same seam
//! [`crate::publish::ReleasePublisher`] uses — declared here, implemented by
//! whoever owns the bytes-in-a-bucket relationship (the `yah` CLI, over R2).
//! A camp with a different reachable store implements this instead; a camp
//! with none gets [`NoBuildContextPublisher`] and a refusal that says so.
//!
//! # The same gap, one layer over: remote subprocess steps (R560-T8)
//!
//! `build_workload_spec` gives an offloaded subprocess exactly three things —
//! the image, the argv, and the `/yah/produced` durable mount. No source. That
//! is fine for `rusty-v8-musl`, whose baked script clones V8 from the internet
//! inside the container, and fatal for any step that compiles the *camp tree*:
//! the `mesofact-musl` legs need `oss/mesofact` plus the two sibling subtrees
//! its path deps escape into. [`pack_source_context`] is the transport,
//! deliberately reusing [`BuildContextPublisher`] rather than growing a second
//! bytes-to-the-worker path — same trait, same single-use run-scoped key, same
//! delete-on-both-legs. The only difference is what goes in the tar and who
//! unpacks it: git-tracked files at their camp-root-relative paths, unpacked
//! by the step's own argv out of `$YAH_SOURCE_CONTEXT_URL`.
//!
//! @yah:relay(R971, "Fleet git remote: a fleet-local push/fetch git server that streams to, and rehydrates from, a private R2 backup (turso-backup's shape, for git)")
//! @yah:at(2026-10-10T23:41:10Z)
//! @yah:assignee()
//! @yah:next("Operator, 2026-10-10 (filed from noisetable, Ashguard session:8c2f4d3c): a git remote living on the fleet that camps push to and workers fetch/checkout from, whose durable copy is a private R2 bucket: every accepted push streams to R2, and a node with an empty disk rehydrates to a serving remote from R2 alone. Model it on oss/turso-backup (stream/tail, snapshot, hydrate, claim/fencing, dedup) rather than inventing a second backup shape.")
//! @yah:next("First consumer is QED's build context. Today (read in build_context.rs) an offloaded step's source is a gzipped tar of the `source_context` paths, uploaded through `BuildContextPublisher` (R2, via the yah CLI) and fetched by the worker from `$YAH_SOURCE_CONTEXT_URL` — a full tar on every dispatch, tracked files only (R855-B1 trap on live workspaces). A git transport gives delta shipping, warm worker clones and a reverse path for worker-side edits for free (see R969-F6/F7).")
//! @yah:next("Self-contained: spike first, then server, backup stream, rehydrate, QED consumer.")
//! @arch:see(.yah/docs/working/W248-wal-streamer-hardening.md)
//! @arch:see(.yah/docs/working/W363-fleet-git-remote.md)
//! @yah:next("LEADER BATON (@Glimmerstone:griffin, session:49c96966). In review: S1, T6, F7, F2 (code), F3. In flight: B9+T8 (session:ec8fca62) fixes yubaba (sovereign group on non-raft nodes; disposable cache volumes skip --where), rolls it (operator F4391), deploys F2's image r971-t8a with no --where, then runs T8's verify. F4 (session:4298376a): hydrate.rs plus the drills; phase 2 builds the first image carrying F3+F4 and redeploys. F5 phase 2 (session:eae987b5): worker-side git materialization, one qed-owned fetch path, and migration of the pipelines' curl|tar. YAH_FLEET_GIT_URL must stay unset until F5's worker wiring and T8's serving address both land. S1 residual: re-run /diligence on W363 once F4 has updated s8. Operator calls so far: F4371 (live infra, drill bounds, git default), F4372 (exec host git, not Alpine-only; mizzle as fallback), F4389/F4390 (one-shot placement on 003, plus the 003 drop-ins), F4391 (fix both yubaba bugs and roll; keep the 003 drop-ins).")
//! @yah:next("LEADER BATON 2026-10-11: in review are S1, T6, F7, F2, F3, T8, B9 (residual struck), B10 (kamaji rolled one node at a time to all five x86 nodes at V13), and F4. fleet-git runs r971-f4a on us-east-001:4330. Both drills pass; cold rehydrate is 48.8-51.5 s and warm ~0.1 s. In flight: R971-B11 (session:a01d312a) fixes and rolls the missing restart/re-place of a killed replicas=1 instance (operator F4397), then reruns the drill with no manual redeploy. The wrap courier (session:1502f55a) moves F5 to review and runs /diligence on W363 s1-s7. Open operator call once F5 is in review: persist YAH_FLEET_GIT_URL camp-wide so git becomes the live default. T8 residual #2 (the hand mkdir on us-east-001) stays while fleet-git runs there.")
//! @yah:next("LEADER BATON 2026-10-11 (2): B11 and F5 are in review. Node-alive recovery is fixed in kamaji (restart.rs) and rolled to 4 of 5 x86 nodes. The live drill recovered with no redeploy in 55.6 s (6.1 s restart + 49 s cold rehydrate). In flight: R971-F12 (session:f10603cf) makes QED resolve the fleet-git address from service records, which is how git becomes the live default (operator F4399), since there is nowhere camp-scoped to persist YAH_FLEET_GIT_URL. R971-B13 (session:1003be64) handles node-gone re-placement (yubaba has no replica reconcile); it first checks for an existing owning relay, then designs, implements and rolls, and also rolls kamaji to us-west-003, which was unreachable for B11. When both are in review, every R971 child is in review.")
//! @yah:next("LEADER FINAL 2026-10-11: every R971 child is in review: S1, T6, F7, F2, F3, T8, B9, B10, F4, F5, B11, F12, B13. Nothing is committed (git policy defer); the operator sweeps it. fleet-git r971-f4a serves on us-east-001:4330, and QED uses git by default, resolving the address from service records. Re-placement on node loss moved to R977 (operator F4400), led by session:d0c3ca53. Residuals: R971-B11 (roll kamaji to us-west-003) and R971-T8 #2 (us-east-001's hand-made mkdir, which stays while fleet-git runs there).")
//!
//! @yah:ticket(R971-S1, "Design: server choice, R2 backup format (pack log + ref journal), fencing, placement, auth")
//! @yah:status(review)
//! @yah:at(2026-10-11T00:35:05Z)
//! @yah:kind(spike)
//! @yah:assignee(agent:bundle-anthropic-glimmerstone)
//! @yah:parent(R971)
//! @yah:next("Decide and write a W-doc: (1) server — bare repos behind smart-HTTP (git-http-backend or a gitoxide-based handler) as a yubaba workload vs an off-the-shelf server; must be musl-buildable and licence-clean for closed-source use; (2) backup format — per-push packfiles content-addressed in R2 plus an append-only ref-update journal, periodic snapshot (repack) to bound rehydrate time; mirror turso-backup's stream/snapshot/hydrate split; (3) single-writer fencing so two nodes never both accept pushes (turso-backup claim.rs epochs); (4) placement — stateful workload, which node(s), what happens when its node is down (rehydrate elsewhere); (5) auth — mesh identity for push/fetch, bucket-scoped R2 pair per the cloudflare SOP, no admin pair; (6) ephemeral refs for live-workspace snapshots (QED runs from a dirty tree) and their GC.")
//! @yah:verify("W-doc with the decisions and a measured rehydrate-time estimate for the yah and noisetable repos.")
//! @yah:next("\"Operator, 2026-10-10: keep the property today's R2 tarball has — a worker far outside the building can still get source. A mesh-reachable server covers every mesh member wherever it sits; decide what serves a fetcher when the server's node is down (read-only fetch straight from the R2 backup format, or the tarball fallback F5 keeps). Also record how today's `$YAH_SOURCE_CONTEXT_URL` is authorized (presigned/expiring vs public) — not yet checked — so the replacement is no weaker.\"")
//! @yah:next("\"Operator, 2026-10-10 — optimise hash→hash deltas, not fan-out: one source hash is built for ~4 platforms, so multi-source distribution (xlb) buys little on the hot path; git's have/want pack negotiation into a warm worker clone is the main win. Where xlb may still pay: cold paths moving one large immutable blob — a fresh worker's first clone from the snapshot pack, and F4 rehydrate from R2 — since snapshot packs are content-addressed and xlb verifies per chunk (BLAKE3/Bao). Evaluate there, not as the per-dispatch transport.\"")
//! @yah:handoff("W363 (.yah/docs/working/W363-fleet-git-remote.md) rewritten to decision state: sections 1-7 each end in a Decision line, section 8 holds the measurements, section 9 maps files to tickets. It started from a same-day draft. Changes made after reading the code and git source: (a) no .idx in R2; rehydrate runs index-pack, which doubles as the integrity check; (b) no latest.journal-head sidecar; the create-only, gap-free journal/<seq:016>.json is the head AND the fork fence (stronger than a head CAS, no orphan-entry window); (c) the hook owns all ref policy and receive.deny* stays unset, because git's builtin/receive-pack.c runs pre-receive BEFORE update(), where the deny_* checks and the update hook run (fetched from git master); (d) snapshots are pack-objects --revs from tips captured under the push lock, not repack -a -d (exact at seq); (e) auth is camp-minted cheers PASETO v4.public (new IssuerKey::Git, git:read/git:write scopes, aud fleet-git/<repo>) verified with cheers-verify, NOT yubaba's v4.local peer key, which would let the workload mint raft-peer tokens; (f) the R2 store is built with object_store AmazonS3Builder like tenant-streamer, not R2ObjectStore::new/from_vault.")
//! @yah:handoff("Server: git http-backend (GPLv2; separate exec'd program; GPL FAQ aggregate rule fetched) from Alpine's git-daemon package (/usr/libexec/git-core/git-http-backend, pkgs.alpinelinux.org v3.22), behind a static-musl Rust supervisor in the new crate oss/yubaba/crates/fleet-git (precedent: tenant-streamer; no new mirror). gitoxide was rejected because its crate-status.md (fetched) leaves server upload-pack/receive-pack and accept() unchecked. Reuse from turso-backup: BackupTarget, claim.rs and probe_conditional_puts, extracted to oss/turso-backup/crates/store-claim, with call sites migrated and no alias. backpressure.rs is not reused; dedup.rs is not applicable.")
//! @yah:handoff("Fencing: the journal Create is the fork fence; store_claim acquire-at-start plus assert_holds per push and every 30 s is the serving fence; fence loss exits 0 under restart_policy on-failure so a fenced node never re-acquires. Placement: archetype server, replicas 1, 3072 MB (index-pack RSS), named volume as a cache, mesh port 4330, no [durability] (kamaji's hydrate hook is turso-only, empty-volume-only, and takes no claim when populated), and startup is always hydrate. Operator asks: (a) the tar fallback serves fetchers when the server is down; direct R2 fetch rejected (private bucket, no SigV4 in git, widens credentials). (b) today's YAH_SOURCE_CONTEXT_URL is a PUBLIC unsigned non-expiring https://cdn.yah.dev/qed-context/<run>-<step>.tar.gz, protected only by the key not being known plus deletion after the step, published with the account-wide admin pair (qed_build_context.rs header, cloud.rs open_r2_bucket -> from_vault); the git path is strictly stronger and the fallback stays as strong as today. (c) no xlb in R971; R2 download is 2-6 s of a 9-22 s CPU-dominated path; revisit when a pack exceeds 2 GiB or F4 measures download above half of hydrate.")
//! @yah:handoff("Ticket updates: F2..F5 each got an append_next with their decisions and replace_files; F2/F5 got gotchas; F4 got an assumes and a gotcha. SHARED FILES for sequencing: F3 and F4 share layout.rs/journal.rs (F3 writes, F4 reads, so land F3 first); F2 and F3 share hook.rs, .yah/services/fleet-git/service.toml and oss/yah-base/crates/keys/src/spec.rs; F2 and F4 share fleet-git/src/main.rs; F2 and F5 share app/yah/cli/src/cloud_cheers.rs. Every depends_on was left as it was; sequencing is the leader's call.")
//! @yah:handoff("Side effects outside the doc: scratch under /tmp/r971-s1/ (yah.git and noisetable.git mirrors plus copied packs, about 0.8 GB), safe to delete. SSH to us-west-002 added its host key to ~/.ssh/known_hosts. Only read-only commands (curl to /dev/null, uname, nproc) ran on us-west-002 and us-west-001. The camp git policy hard-blocks git add/commit/init even in /tmp, so the planned pre-receive ordering experiment was replaced by reading git's receive-pack.c and environment.h. No camp-repo git writes and no Rust edits.")
//! @yah:verify("camp.machine at 17:19 returned quiet:false (15 foreign live slots in noisetable, 12 rustc/cargo procs, load 14.0/20.5/21.6 on 15 CPUs; load 11-27 during each timed step), so wall-clock numbers are contaminated upward. All git timings ran on the camp Mac against /tmp/r971-s1 scratch mirrors.")
//! @yah:verify("Sizes (git count-objects -vH): live yah = 48,393 objs in 19 packs, 299.3 MiB, plus 251.1 MiB loose; live noisetable = 73,355 in 12 packs, 328.2 MiB. Snapshot-equivalent single pack via clone --mirror --no-local: yah 43,598 objs / 139.5 MiB (51 refs), noisetable 71,975 objs / 258.6 MiB (97 refs).")
//! @yah:verify("Mac timings: clone --mirror --no-local yah 40.0 s real / 44.3 s user, noisetable 6.2 s / 11.7 s. index-pack of the snapshot pack: yah 8.9 s / 19.8 s user / 2.38 GB max RSS; noisetable 4.8 s / 7.4 s / 0.89 GB. fsck --connectivity-only 0.06 s / 0.08 s. Full fsck 13.2 s / 8.3 s. repack -a -d of a single-pack mirror 0.64 s / 2.18 s (verbatim reuse, not representative).")
//! @yah:verify("R2 throughput: cache-busted GET of an 81.2 MB object on cdn.yah.dev (cf-cache-status MISS, so origin is R2): camp Mac 39-63 MB/s over 3 runs; fleet node us-west-002 (x86_64, 16 CPU) 52-63 MB/s over 2 runs; fleet node us-west-001 (x86_64, 6 CPU) 43-69 MB/s over 2 runs; Mac cache HIT 53-62 MB/s. The private-bucket S3 endpoint itself was NOT measured; the CDN MISS is a proxy.")
//! @yah:verify("Rehydrate estimate, cold from a fresh snapshot: yah ~11-13 s and noisetable ~9-11 s on Mac-class CPU (fleet-measured download plus Mac index-pack plus connectivity fsck); extrapolated ~20-22 s and ~14-17 s on a fleet node, assuming cores at most 2x slower (a guess, not measured). The worst-case journal tail at the 256 MiB snapshot bound adds ~9-22 s. yubaba detection+reschedule time was not measured; the F4 drill owns it.")
//! @yah:verify(".yah/db/*.turso check: 429 blob versions; task-runs.turso is 99.3 MB uncompressed but 36-487 KB per version on disk; 32.2 MiB of 136.3 MiB blob disk. They are inferred, not proven, to drive yah's index-pack RSS.")
//!
//! @yah:ticket(R971-F2, "Fleet git server workload: push/fetch over the mesh, mesh-identity auth, single-writer fenced")
//! @yah:status(review)
//! @yah:at(2026-10-11T02:15:02Z)
//! @yah:assignee(agent:bundle-anthropic-ashguard)
//! @yah:parent(R971)
//! @yah:next("Implement the server S1 chose as a yubaba-managed stateful workload: `git push fleet` from a camp and `git fetch`/checkout from a worker over the mesh; refuses pushes unless it holds the writer claim.")
//! @yah:next("S1 DECIDED (W363 s1,s3,s5,s9). (1) Extract BackupTarget+join_key, all of claim.rs, probe_conditional_puts+PreconditionSupport+PreflightStage into oss/turso-backup/crates/store-claim (object_store+anyhow only); make turso-backup a workspace; fix every importer in place, no re-export alias. (2) New crate oss/yubaba/crates/fleet-git (lib+bin): axum supervisor exec'ing Alpine git-daemon's /usr/libexec/git-core/git-http-backend per request (GIT_PROJECT_ROOT, GIT_HTTP_EXPORT_ALL, REMOTE_USER=token sub, GIT_PROTOCOL from Git-Protocol header); per-repo push lock across POST git-receive-pack; repo config receive.unpackLimit=1, receive.fsckObjects=true, http.receivepack=true, pack.threads=2, NO receive.deny* (hook owns policy: heads/tags ff-only never deleted, refs/qed/** free, other namespaces refused). pre/post-receive = fleet-git in hook mode over $FLEET_GIT_SOCK (F3 fills the durable step). Fence: probe + acquire at start; assert_holds per push and every 30 s; on loss close listener, exit 0. (3) Auth: cheers v4.public via cheers_verify::PasetoV4PublicVerifier, iss yah-camp, aud fleet-git/<repo> must match URL repo, receive-pack needs git:write; verify key = cluster secret cheers/fleet-git/verify-key (file mount); add GIT_READ/GIT_WRITE + 'git' namespace to yah_scopes (update W159 vocab test); add IssuerKey::Git (cheers-git-signing-key/cheers-git-verify-key) + 5-min push-token mint in cloud_cheers.rs (operator may override: reuse Vend). Client passes token via GIT_CONFIG_COUNT/KEY_0=http.extraHeader/VALUE_0 env, never argv. (4) .yah/services/fleet-git: container, archetype server, replicas 1, restart_policy on-failure, memory_mb 3072, cpu_millis 1000, named volume fleet-git-repos at /var/lib/fleet-git (a cache), expose.mesh.ports [4330], NO [durability], no host selector. Dockerfile: musl builder -> alpine + apk add git git-daemon.")
//! @yah:gotcha("S1: oss/turso-backup/src/stream.rs is the file that lost 827 uncommitted lines on 2026-08-28 - edit only its import hunks, with the Edit tool. oss/yah-base/crates/keys/src/spec.rs was dirty (uncommitted, peer unknown) on 2026-10-10 - check camp.roster before touching it. Shares hook.rs / service.toml / keys spec.rs with F3 and cloud_cheers.rs with F5.")
//! @yah:next("Tier: Cleric. LEADER RE-CUT 2026-10-10: decision (1) store-claim extraction moved to R971-T6; the auth half of decision (3) (scopes, IssuerKey::Git, mint in cloud_cheers.rs, keys spec) moved to R971-F7. F2 is now the fleet-git crate (supervisor, CGI exec, hook mode, verifier side of auth via cheers_verify, fencing via store-claim) plus .yah/services/fleet-git; it consumes T6's store-claim and F7's mint/verify-key as finished surfaces.")
//! @yah:depends_on(R971-S1)
//! @yah:depends_on(R971-T6)
//! @yah:depends_on(R971-F7)
//! @yah:files(oss/yubaba/Cargo.toml)
//! @yah:files(oss/yubaba/crates/fleet-git/Cargo.toml)
//! @yah:files(oss/yubaba/crates/fleet-git/Dockerfile)
//! @yah:files(oss/yubaba/crates/fleet-git/src/lib.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/main.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/serve.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/cgi.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/auth.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/hook.rs)
//! @yah:files(.yah/services/fleet-git/service.toml)
//! @yah:files(.yah/services/fleet-git/mirrors/prod.toml)
//! @yah:next("OPERATOR CALLS 2026-10-10 (form F4371/F4372). (a) ENGINE: exec git's own `git http-backend` (gitoxide confirmed to have no server side and no client push - crate-status.md), BUT NOT ALPINE-ONLY: the supervisor must find it via `git --exec-path` (never a hardcoded /usr/libexec/git-core path), work on glibc distros (Debian/Ubuntu, OrbStack's libc-based distros) as well as Alpine, and probe at startup (git present, http-backend executable, minimum git version) and exit non-zero with a clear message on failure. The operator would rather rely on the host OS having git than vendor it; keep the workload able to run natively on a host with system git, not only from the container image. Fallback if exec-git ever proves unworkable: fork/adopt github.com/willstott101/mizzle (operator-named; we would co-own it) - not now. (b) LIVE INFRA APPROVED: generate the cheers-git keypair into cluster secrets, and deploy the fleet-git service to prod to meet F2's verify (push from the camp Mac, clone on us-west-003).")
//! @yah:handoff("STEP 0 re-verification of T6/F7, all PASS: turso-backup --workspace 198 passed / 0 failed (11 store-claim + 162 lib + bins 4/5/4/5 + rss 7; claim was 173 for lib+store-claim and it holds) /tmp/r971-f2-1.log; yubaba-tenant-streamer 36/0 (-2.log); cheers-core 147+2/0 (-3.log); fob 64/0 (-4.log); yah --lib cloud_cheers 10/0 (-5.log); cargo check -p yah EXIT=0 (-6.log, advisory skew: peer edited qed build_context.rs mid-run).")
//! @yah:handoff("W363 s1 updated per form F4372: host's own git http-backend via `git --exec-path`, never hardcoded; glibc + Alpine + native; startup probe (git on PATH, http-backend executable, git >= 2.25, non-zero exit naming the package); mizzle recorded as operator-named fallback, not adopted. Decision line rewritten.")
//! @yah:handoff("NEW CRATE oss/yubaba/crates/fleet-git (lib + bin; added to oss/yubaba/Cargo.toml members): gitbin.rs (probe, MIN_GIT 2.25), repo.rs (name validation, bare init + symbolic-ref HEAD refs/heads/main since init -b needs 2.28, REPO_CONFIG unpackLimit=1/fsckObjects/http.receivepack/pack.threads=2, no receive.deny*, hooks re-installed every start pointing at current_exe), auth.rs (PasetoV4PublicVerifier, kid derived from key, iss yah-camp, aud fleet-git/<repo>, git:read always, git:write for receive-pack; key file raw 32 bytes or 64 hex), fence.rs (probe_conditional_puts -> Degraded refuses start; acquire -> Lost exits non-zero; check() = Holds/Lost/Unknown; 30 s timer), hook.rs (thin client over $FLEET_GIT_SOCK, JSON line protocol; policy heads/tags ff-only never deleted, refs/qed/** free, others refused; old must equal server's ref; then assert_holds; then PushLog::append = THE R971-F3 SEAM, F2 ships `Unjournaled` no-op and logs a warning at startup), cgi.rs (http-backend child, CGI env incl GIT_PROTOCOL, HTTP_CONTENT_ENCODING, env_clear + HOME=root + GIT_CONFIG_NOSYSTEM; push lock guard released only when the child exits so it spans hooks), serve.rs (axum, /healthz, smart-HTTP only, 401/403, first push creates repo, fence loss -> graceful shutdown -> ServeExit::FenceLost), main.rs (serve/hook/probe; S3 store with ETagMatch from *_FILE keys, --store memory for local runs, --prefix; listen = --listen > YAH_MESH_IP:PORT(4330) > 127.0.0.1, never wildcard; fence loss exits 0).")
//! @yah:handoff("DECISION recorded in fence.rs: an UNREADABLE claim store (io error) refuses the push but does not end the process; only a definitive ClaimLost does. Otherwise a transient R2 blip would exit 0 and leave the single replica stopped under on-failure.")
//! @yah:handoff("Client: `yah cloud cheers git --repo <r> [--sub] -- <git args>` (cloud_cheers.rs CheersCommands::Git + git_exec + git_auth_env; dispatch arm in cloud.rs handle_cheers). Token only via GIT_CONFIG_COUNT/KEY_0=http.extraHeader/VALUE_0 env. Built into target/opt-in/bin/yah via cargo xtask install (EXIT=0, /tmp/r971-f2-9.log).")
//! @yah:handoff("Service: .yah/services/fleet-git/service.toml (container, infra tier, server, replicas 1, on_failure max 10 backoff, 3072 MB, 1000 mcpu, named volume fleet-git-repos at /var/lib/fleet-git, mesh port 4330, host network like cloud-admin, verify-key + R2 pair as file secrets, /healthz healthcheck, no [durability], no host). mirrors/prod.toml: class selector mesh-tags arch:x86 only (image is amd64), image tag/digest = PENDING. New secret specs .yah/infra/secrets/yah-fleet-git-r2-{access-key-id,secret-key}.toml (vault slots yah-fleet-git-{access-key-id,secret-key}, which is what `cf token create --name yah-fleet-git` writes). Dockerfile: alpine:3.22 + apk add git git-daemon, COPY of the host-built musl binary, build-time `fleet-git probe`. Deviation from W363: no musl builder stage, because the host cross-build is native speed.")
//! @yah:handoff("musl x86_64 binary built: oss/yubaba/target/x86_64-unknown-linux-musl/release/fleet-git (static-pie, /tmp/r971-f2-10.log).")
//! @yah:handoff("NOTHING LIVE DONE. Every Bash call after the opt-in install timed out at the yah-gate approval form (10 min, form cancelled, three times), so the image build, keygen, bucket, token, secret put and deploy were never attempted. No git writes.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("Image: stage the musl binary into /tmp/r971-f2/img-ctx/fleet-git; `docker buildx build --platform linux/amd64 -f oss/yubaba/crates/fleet-git/Dockerfile --output type=oci,dest=/tmp/r971-f2/oci-<tag>,tar=false /tmp/r971-f2/img-ctx`; then `yah cloud cr push <dir> --name fleet-git --tag <tag>` and pin tag + digest in .yah/services/fleet-git/mirrors/prod.toml (replace PENDING). The buildx OCI dir has not been tried with cr push; check that cr push accepts it.")
//! @yah:next("Keys, operator-approved on F4371: `target/opt-in/bin/yah cloud cheers init --key git` (the installed yah lacks --key git), then `yah cloud secret put cheers-fleet-git-verify-key`.")
//! @yah:next("Bucket: create the private R2 bucket yah-fleet-git with POST /accounts/3948dc292e724e71b0deefde0ea95999/r2/buckets using vault slot cloudflare-api-token (W295 precedent; `yah cloud bucket create` is Hetzner-only), with no custom domain and r2.dev disabled. Then `yah cloud cf token create --profile r2-bucket --bucket yah-fleet-git --access rw --name yah-fleet-git --bootstrap-slot cloudflare-legacy-yah`; register both slots in oss/yah-base/crates/keys/src/spec.rs (sorted, your hunks only, the file is peer-dirty); `yah cloud secret put yah-fleet-git-r2-access-key-id` and `... -r2-secret-key`; board.update R971-F3 noting that the bucket and pair moved to F2.")
//! @yah:next("Deploy: `yah cloud workload deploy fleet-git` with a tree-built CLI (the installed yah may refuse the current spec shape; see the cloud-admin mirror gotcha). Never roll yubaba/kamaji; if the deploy needs a fleet-binary roll, stop and report. `yah cloud validate` the spec first, since service.toml has never been parsed by the loader.")
//! @yah:next("Verify: set the remote `git remote add fleet http://<placed mesh ip>:4330/yah.git` (read it from GET /service-records); `target/opt-in/bin/yah cloud cheers git --repo yah -- push fleet main`; on us-west-003 clone with a git_read_token passed the same way via env; compare `git ls-remote` refs. Optional real-store fence check: run two local `fleet-git serve --prefix test/r971-f2` instances against the R2 bucket; the first must refuse a push after the second starts.")
//! @yah:verify("cd oss/yubaba && cargo test -p fleet-git: 10 unit + 1 e2e passed, 0 failed (/tmp/r971-f2-8.log). The e2e test (tests/roundtrip.rs) uses this Mac's non-Alpine system git (Apple Git-155, 2.50.1) through the real CGI + hook path. It covers: push refused with no token and with a read-only token; push then clone with matching HEAD; force-push, branch delete and refs/notes refused; refs/qed create/force/delete accepted. A second instance acquiring the claim on a shared in-memory store makes the first refuse the next push ('writer claim'), and the first's serve() returns FenceLost. The second accepts the same history and keeps serving.")
//! @yah:gotcha("\"The yah-gate approval form timed out (10 min) on every Bash call from about the 40th call on, including trivial mkdir/cp into /tmp/r971-f2. Live steps need an operator present to approve.\"")
//! @yah:handoff("LEADER 2026-10-10: F2 is closed as code-complete. The crate, client, service spec and musl binary landed, and the local e2e passes, covering auth, ref policy and fencing on a shared in-memory store. Its live half (image, keys, bucket, token, secrets, deploy, Mac-push/us-west-003-clone verify) moved verbatim to R971-T8, which depends on F2. The next steps above (Image/Keys/Bucket/Deploy/Verify) are T8's brief now, not F2's.")
//! @yah:verify("Step 0 re-verified T6/F7, all passing: turso-backup --workspace 198/0, tenant-streamer 36/0, cheers-core 147+2/0, fob 64/0, cloud_cheers 10/0, check -p yah exit 0. fleet-git: 10 unit tests + 1 e2e pass, 0 fail (/tmp/r971-f2-8.log). The e2e test drives this Mac's non-Alpine system git (Apple Git 2.50.1) through the real CGI and hook path and covers auth, ref policy, and fence handover. The live verify moved to R971-T8.")
//!
//! @yah:ticket(R971-F3, "Stream every accepted push to private R2 (packs + ref journal, deduped) with periodic snapshot")
//! @yah:status(review)
//! @yah:at(2026-10-11T03:17:47Z)
//! @yah:assignee(agent:bundle-anthropic-ashguard)
//! @yah:parent(R971)
//! @yah:next("A push is acknowledged durable only once its pack + ref-journal entry are in R2 (or say explicitly which ack semantics S1 chose). Backpressure and dedup modelled on oss/turso-backup (backpressure.rs, dedup.rs). Periodic snapshot/repack bounds rehydrate cost.")
//! @yah:verify("After N pushes the bucket holds exactly the packs + journal entries for them (no duplicates on re-push); killing the server mid-push leaves R2 consistent.")
//! @yah:depends_on(R971-F2)
//! @yah:next("S1 DECIDED (W363 s2,s5) - SUPERSEDES the backpressure/dedup clause above: backpressure.rs NOT reused (inline 429/503 retry, max 3 tries, then fail the push); dedup.rs N/A (pack negotiation is the dedup). Bucket yah-fleet-git (private; create it first) + pair via `yah cloud cf token create --profile r2-bucket --bucket yah-fleet-git --access rw --name yah-fleet-git`; cluster secrets yah/fleet-git/r2-{access-key-id,secret-key}, file-mounted, read via _FILE env; store via object_store AmazonS3Builder with explicit keys (as tenant-streamer does), never from_vault. Layout (prefix ''): repos/<repo>/packs/<pack-name>.pack (Create; AlreadyExists=ok; NO .idx); repos/<repo>/journal/<seq:016>.json (Create, gap-free seq from 1 = the head AND the fork fence; NO latest.journal-head); snapshots/<seq:016>/<pack>.pack then refs.json (Create); latest.snapshot {seq,pack} CAS'd last. Journal v1 schema in W363 s2. Durable step (supervisor, under push lock): policy -> assert_holds -> PUT pack from $GIT_QUARANTINE_PATH/pack -> Create journal/<seq+1> (AlreadyExists => refuse push, enter fence loss) -> exit 0; post-receive writes <repo>.git/fleet-git-state.json {seq, refs_digest}. Snapshot: capture seq + for-each-ref under the lock; `git pack-objects --revs --delta-base-offset` from those tips; trigger at 500 entries or 256 MiB, at least 10 min apart; then local `git repack -a -d -l` under the lock. Sweep after latest.snapshot moves: keep 2 snapshots; delete journal+packs with seq <= the older kept snapshot; delete unreferenced packs older than 1 h. Ephemeral GC: delete refs/qed/* whose creating entry is >7 d old, as an internal journaled update (pusher fleet-git:gc, pack null).")
//! @yah:next("Tier: Cleric. Leader: F3 also touches oss/yah-base/crates/keys/src/spec.rs, which R971-F7 edits; F3 is gated on F2, which depends on F7, so that order is already enforced.")
//! @yah:next("OPERATOR CALL 2026-10-10 (form F4371): APPROVED to create the private R2 bucket yah-fleet-git and its bucket-scoped rw token (`yah cloud cf token create --profile r2-bucket ...`), and to store the pair as cluster secrets. Never use the admin pair.")
//! @yah:next("LEADER 2026-10-10: the bucket, token, R2 secret specs, keys spec.rs slots and service.toml now belong to R971-T8 (the live bring-up), so F3 doesn't touch them. F3 is the code inside oss/yubaba/crates/fleet-git: replace F2's `PushLog` / `Unjournaled` no-op seam in hook.rs with the durable step, plus layout/journal/stream/snapshot/sweep and ephemeral refs/qed GC. Test against object_store InMemory (main.rs already has --store memory). Durability is proven against the real bucket after T8 lands, as part of F4's drill.")
//! @yah:files(oss/yubaba/crates/fleet-git/src/layout.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/journal.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/stream.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/snapshot.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/sweep.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/hook.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/serve.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/main.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/lib.rs)
//! @yah:handoff("Ashguard (session:a366f0cf) claimed 2026-10-10, took the baseline: cd oss/yubaba && cargo test -p fleet-git, which passed 10 unit tests and 1 e2e (/tmp/r971-f3-1.log). Read W363 s2/s3/s6 plus hook.rs, fence.rs, main.rs, repo.rs, the head of serve.rs and tests/roundtrip.rs. NO code edits were made. Stopped because the same read-only Bash call (the rest of serve.rs plus the store-claim API grep) hit a yah-gate approval timeout twice in a row, so the operator is away.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("Resume at design: read serve.rs lines 120-249 (the push lock and the hook wiring) and the store-claim pub API (BackupTarget join_key). Then implement layout.rs, journal.rs, stream.rs (a PushLog impl that replaces Unjournaled in main.rs and tests), snapshot.rs and sweep.rs, plus the e2e cases from the courier brief.")
//! @yah:handoff("R971-T8 2026-10-11: private R2 bucket `yah-fleet-git` is LIVE (WNAM, r2.dev disabled, no custom domain). Bucket-scoped rw token `yah-fleet-git` (CF id 316b84404464c5ceb77712b98c2e7665, expires 2027-10-11) is in vault slots yah-fleet-git-access-key-id / yah-fleet-git-secret-key (CredentialSpecs added in oss/yah-base/crates/keys/src/spec.rs) and shipped as cluster secrets yah/fleet-git/r2-access-key-id + yah/fleet-git/r2-secret-key (group prod, consumer fleet-git). The bucket and pair are owned by F2/T8 now; F3 journals into this same bucket.")
//! @yah:handoff("Ashguard (session:e9f43fe5) landed F3, uncommitted, inside oss/yubaba/crates/fleet-git only. NEW: src/layout.rs (W363 s2 keys, zero-padded seq, parsers, pack-name check), src/journal.rs (v1 JournalEntry, append = PUT Create journal/<seq:016>.json with AlreadyExists -> AppendError::Moved, head/list/entries_after, inline 429/503 retry with MAX_TRIES=3 via error-text match, put_create_idempotent), src/stream.rs (R2Log: the PushLog impl; seq comes from the volume marker <repo>.git/fleet-git-state.json {seq, refs_digest = git hash-object of for-each-ref}; pack PUT from $GIT_QUARANTINE_PATH/pack, then journal Create, and Moved calls Fence::lose; committed writes the marker; internal_update for GC journals then runs git update-ref --stdin; unapplied() = entries > marker.seq for crash detection; FailPoint crash injection, used by tests), src/snapshot.rs (due() at 500 entries / 256 MiB / 10 min; take() captures marker seq + for-each-ref under the lock, runs pack-objects --revs --delta-base-offset, Creates the snapshot pack then refs.json, CASes latest.snapshot, then repack -a -d -l under the lock; maintain() is a 5-min loop doing qed GC, then snapshot if due, then sweep), src/sweep.rs (keeps latest + newest complete older snapshot; deletes journal entries and their packs at or below the older snapshot, other snapshot dirs below latest or abandoned >1h, and unreferenced packs >1h; gc_ephemeral deletes refs/qed/* whose creating entry is >7d old, using the oldest retained entry's age when the sweep already removed it; R971-F5 chaining is noted in the module doc).")
//! @yah:handoff("EDITED: hook.rs (Unjournaled deleted; HookRequest/AcceptedPush gain `pusher` from $REMOTE_USER), fence.rs (Fence::lose for journal fork-fence loss), serve.rs (the push-lock map extracted as pub PushLocks and passed in through ServeConfig.locks so the snapshot shares it), lib.rs (modules), main.rs (builds R2Log on the same store/prefix as the fence, spawns snapshot::maintain, drops the F3-not-landed warning). tests/roundtrip.rs: in the existing fence test, instance B now pushes repo `fresh` instead of `yah`. B's volume has no replay of A's yah journal (that is F4's hydrate), so a yah push from B would correctly lose the fork fence. Added 4 e2e cases.")
//! @yah:handoff("Not done / for F4 and T8: no startup replay or hydrate (F4); a volume behind the journal makes the next append hit Moved and lose the fence until F4 replays. Packs are read whole into memory before the PUT (no multipart). gc_ephemeral reads every retained journal entry on each 5-min tick. Throttle detection matches on error text because object_store 0.14 exposes no status field.")
//! @yah:verify("cd oss/yubaba && cargo test -p fleet-git (/tmp/r971-f3b-3.log, EXIT=0): 17 unit tests passed (baseline 10; +7 new: layout keys, journal schema/fork-fence/retry, stream marker+quarantine, snapshot trigger, sweep retained set) and 5 e2e passed (baseline 1; +4: n_pushes_leave_n_packs_and_n_entries incl. the qed GC, crashes_leave_the_store_consistent, racing_writers_one_wins_the_loser_refuses, snapshot_then_sweep_leaves_the_retained_set). 0 failed, no warnings. The skew advisory on that run was my own in-command edit to roundtrip.rs before cargo started.")
//! @yah:verify("Real-bucket durability is still owed to F4's drill after T8.")
//!
//! @yah:ticket(R971-F4, "Rehydrate: an empty node restores a serving fleet remote from R2 alone, then takes the writer claim")
//! @yah:status(review)
//! @yah:at(2026-10-11T05:25:53Z)
//! @yah:assignee(agent:bundle-anthropic-ashguard)
//! @yah:parent(R971)
//! @yah:next("Snapshot + journal replay to the latest ref state; claim/fence handover so the old node, if it comes back, cannot accept pushes (turso-backup hydrate.rs + claim.rs). Drill it as a QED recipe so it is rehearsed, not assumed.")
//! @yah:verify("QED drill: wipe the serving node's disk (or start on a fresh node), rehydrate, `git fsck` clean, refs equal the pre-wipe journal head; the old node refuses pushes after handover.")
//! @yah:next("S1 DECIDED (W363 s3,s4,s8). Supervisor startup is the ONLY rehydrate path, and it runs on EVERY start: probe_conditional_puts (Degraded => refuse) -> acquire(owner fleet-git@<node>) (Lost => exit non-zero) -> per repo: WARM if marker present, refs_digest matches, and journal/<marker.seq+1> exists or nothing follows => replay entries > marker.seq; else COLD: wipe, git init --bare with the s1 config, stream latest.snapshot's pack, git index-pack (integrity check + idx), refs from refs.json, replay entries > snapshot.seq (each pack downloaded + index-pack'd) -> assert_holds (Superseded => exit 0) -> git fsck --connectivity-only (fail => one cold retry, then exit non-zero) -> serve. Replay = update-ref --stdin (no hooks); each ref must be at old or new, else go cold. No full fsck at startup. Do NOT use kamaji's [durability] / turso-backup-hydrate hook (turso-only, empty-volume-only, takes no claim when populated). Budget to beat (W363 s8): cold from a fresh snapshot ~11-13 s yah / ~9-11 s noisetable on Mac-class CPU; extrapolated ~20-22 s / ~14-17 s on a fleet node. Re-measure on a node and replace the <=2x guess; also measure yubaba's detection+reschedule gap. Drills: .yah/qed/fleet-git-rehydrate-drill.toml (wipe volume / kill node -> replica comes up elsewhere, fsck clean, refs == journal state, old node exits 0) and fleet-git-crash-drill.toml (kill between journal Create and ref update -> restart -> refs == journal state).")
//! @yah:gotcha("S1: reads layout.rs/journal.rs, which F3 writes - land F4 after F3. main.rs is shared with F2.")
//! @yah:assumes("S1 (unverified): after a dead node returns, yubaba's replicas=1 reconcile leaves exactly one fleet-git instance running, and an instance that exits 0 on fence loss is not restarted under restart_policy on-failure. The rehydrate drill must prove both - kill a node, let it come back, observe a single serving instance.")
//! @yah:files(oss/yubaba/crates/fleet-git/src/hydrate.rs)
//! @yah:files(oss/yubaba/crates/fleet-git/src/main.rs)
//! @yah:files(.yah/qed/fleet-git-rehydrate-drill.toml)
//! @yah:files(.yah/qed/fleet-git-crash-drill.toml)
//! @yah:next("Tier: Cleric. Leader: depends_on F3 added - F4 reads layout.rs/journal.rs that F3 writes.")
//! @yah:next("OPERATOR CALL 2026-10-10 (form F4371): the rehydrate drill MAY wipe the fleet-git volume and kill its instance so the replica reschedules, and runs live against prod. It may NOT power-kill a node; weekly chaos policy already covers node loss.")
//! @yah:next("LEADER: F3 is in review. The code half of F4 (hydrate.rs plus the startup path in main.rs) starts now. The live half (new image, redeploy, drills) waits on R971-T8, which in turn waits on R971-B9's yubaba roll. That is a soft edge, so it doesn't block the code. F3's handoff shows that a volume behind the journal makes the next append hit Moved and lose the fence until F4 replays, so F3's code must NOT be deployed without F4's. The first image to carry F3 is the one F4 builds.")
//! @yah:handoff("Ashguard (session:4298376a) landed PHASE 1, uncommitted, inside oss/yubaba/crates/fleet-git plus two .yah/qed recipes. NEW src/hydrate.rs: startup() = per repo (union of store repos/ and volume) hydrate() -> fence.check() (Lost => Startup::Superseded, caller exits 0; Unknown => error) -> git fsck --connectivity-only per repo, one forced-cold retry, then error. hydrate(): WARM iff HEAD exists, marker present, refs_digest matches, and first journal seq after marker is marker.seq+1 or none; replay; a WARM replay that diverges falls through to COLD. COLD: remove_dir_all, repo::ensure (F2 config + hooks), stream latest.snapshot pack to objects/pack + git index-pack, update-ref --stdin creates from refs.json, refuse if the journal tail does not start at base+1, replay. replay/apply: per entry download+index-pack its pack (skipped if .pack+.idx already on disk), each ref must be at old (applied) or new (skipped) else error, git update-ref --stdin (no hooks), marker rewritten after each entry. Reuses journal::list_seqs/read, layout keys, stream marker/refs_digest/for_each_ref, snapshot::read_latest/SnapshotRef; snapshot::git_ok made pub(crate). main.rs: Fence::establish (probe + acquire) -> hydrate::startup -> serve; logs per-repo mode/base/replayed/seq/ms.")
//! @yah:handoff("tests/roundtrip.rs: start() now runs hydrate::startup and records reports. F3's fence-test workaround is reverted: instance B starts on an empty volume, cold-hydrates A's yah (asserts Mode::Cold at journal head, ls-remote main == A's tip), pushes yah, and A refuses and exits FenceLost. Added cold_start_from_snapshot_and_journal_tail, warm_start_with_a_lagging_marker_replays_only_the_tail, and crash_between_journal_create_and_ref_update_then_restart. Recipes .yah/qed/fleet-git-rehydrate-drill.toml and fleet-git-crash-drill.toml are local-host steps running those e2e cases (both listed by `yah qed pipelines`). Their headers state that the live steps are not written yet. No services/*, yubaba or kamaji files were touched, and YAH_FLEET_GIT_URL was not set.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("PHASE 2 (live), only once R971-T8 is in review with fleet-git serving (still `handoff`, blocked on R971-B9, at F4's phase-1 end): build musl fleet-git from the tree (F2+F3+F4), push a new cr.yah.dev/fleet-git tag, re-pin .yah/services/fleet-git/mirrors/prod.toml, redeploy WITHOUT --where; push yah; add live steps to both drill recipes and run them (F4371: may wipe the fleet-git volume and kill its instance; never power-kill or pin a node). Measure cold rehydrate on the node (startup logs 'rehydrated ... ms=') and yubaba detection+reschedule gap; replace the <=2x guess in W363 s8. Prove exactly one serving instance after return and that an exit-0 fence loss is not restarted. Do NOT export YAH_FLEET_GIT_URL (R971-F5).")
//! @yah:verify("cd oss/yubaba && cargo test -p fleet-git (/tmp/r971-f4-3.log, EXIT=0): 17 unit (baseline 17) + 8 e2e (baseline 5; +3 new, and the fence test now pushes yah from B). /tmp/r971-f4-4.log: crash-drill filter selects 2 tests, both pass.")
//! @yah:next("LEADER: once R971-T8 is in review, one Cleric courier runs F4 phase 2 (tree-built image carrying F2+F3+F4, re-pin, redeploy with no --where, add the live steps to both drill recipes, run the drills, measure, update W363 s8) and then R971-F5's live check against that image, in one session.")
//! @yah:depends_on(R971-F2)
//! @yah:depends_on(R971-F3)
//! @yah:depends_on(R971-T8)
//! @yah:depends_on(R971-B10)
//! @yah:handoff("PHASE 2 LIVE. Built musl fleet-git from the tree (F2+F3+F4), pushed cr.yah.dev/fleet-git:r971-f4a sha256:ce0b471b6e39c525c6fc487e4979d79acdd5fe740af2f50239e9e6be550870c9, re-pinned .yah/services/fleet-git/mirrors/prod.toml, and redeployed with no --where. Placement chose us-east-001 again. Pushed yah main (2f1470ad) through it. Live steps went into both drill recipes as a `live` step calling the new scripts/fleet-git-live-drill.sh {rehydrate|crash}. Both pipelines PASSED live: fleet-git-rehydrate-drill run 89d84b0a, fleet-git-crash-drill run c2cbb03b. W363 s8 and the s5 serving-gap line now carry the fleet measurements in place of the <=2x guess.")
//! @yah:verify("REHYDRATE (live): volume wiped + task SIGKILLed, then redeployed with no --where. Cold rehydrate ms=51451/51525/48813 (journal-only: no snapshot yet, entry 1 = 140 MB full pack). Refs == pre-wipe head, fsck --connectivity-only clean, serving count 1. HANDOVER: a second instance (exec'd in the container, port 4331) took epoch 6. The original logged 'writer claim lost; exiting 0', the task went STOPPED and was not restarted, and the no---where redeploy took epoch 7 warm in 102 ms with refs equal and fsck clean, leaving exactly one instance serving. CRASH: SIGKILL right after the push was acked; the branch survived, pre-existing refs were unchanged, and a second restart left refs unchanged (warm 101-103 ms). cargo test -p fleet-git: 17 unit + 8 e2e, matching baseline.")
//! @yah:gotcha("MEASURED: a killed containerd task is NOT restarted. kamaji shows it 'Failed' indefinitely despite restart_policy on_failure, and yubaba (replicas=1) does not reschedule. The drill re-places it with `yah cloud workload deploy fleet-git` (no --where). Until that is fixed, any crash of fleet-git is an outage that waits for a human. This needs its own ticket: it is a kamaji or yubaba bug, not fleet-git's.")
//! @yah:assumes("The crash drill's SIGKILL landed after the push was acked (ssh latency is longer than the push), so the exact window between journal Create and ref update is proven only by the e2e test crash_between_journal_create_and_ref_update_then_restart, not live. Also: fleet-git never moved off us-east-001, so the T8 hand-made volume dir stays (T8 residual #2 is not struck), and B10's 'fresh placement on a node with no pre-created dir' has not yet been exercised live. The claim owner reads fleet-git@localhost: the hostname inside the container is localhost.")
//! @yah:cleanup("Remote refs refs/heads/drill/crash-20261011T051438 and refs/heads/drill/crash-20261011T052112 (both at 2f1470ad) are left on fleet-git yah: the pre-receive policy refuses deletes with the push token. Delete them with a token that may delete, or allow drill/* deletion.")
//!
//! @yah:ticket(R971-F5, "QED build context over the fleet remote: push the run's tree as an ephemeral ref, worker fetches into a warm clone (replaces the per-dispatch tarball)")
//! @yah:status(review)
//! @yah:at(2026-10-11T07:34:12Z)
//! @yah:assignee(agent:bundle-anthropic-miravel)
//! @yah:parent(R971)
//! @yah:next("A second `BuildContextPublisher`-shaped transport: the coordinator snapshots the run's tree (including a dirty live workspace, as an ephemeral commit, which closes the R855-B1 untracked-file trap) to a run-scoped ref on the fleet remote; the worker fetches only missing objects into a persistent clone and checks out. Keep the tar transport as fallback when the remote is unreachable. This is the substrate R969-F6 (delta re-ship on fix-and-resume) and R969-F7 (worker edits back as a diff/commit) should build on rather than inventing their own transfer.")
//! @yah:verify("Second dispatch of the same pipeline after a one-file edit transfers only that object; yah-cli-release's offloaded musl leg goes green using it; ephemeral refs are GC'd.")
//! @yah:depends_on(R971-F2)
//! @yah:next("S1 DECIDED (W363 s6,s7). Camp-side staging repo .yah/cache/fleet-git/<repo>.git (bare; objects/info/alternates -> camp .git/objects; .yah/cache is already gitignored), so NOTHING is written to the camp's index/objects/refs. Per remote step, resolve <camp-commit> = the camp's checked-out commit SHA at snapshot time, then with GIT_DIR=staging GIT_WORK_TREE=<camp> GIT_INDEX_FILE=<tmp>: `git ls-tree -r <camp-commit> -- <source_context> | git update-index --index-info`; `git add -A -- <source_context>` (closes R855-B1); write-tree; commit-tree with NO parent, message `yah-snapshot-of: <camp-commit> dirty=<bool>`; push <commit>:refs/qed/<run-id>/<step-id> with the push token. The staging repo keeps refs/qed/* so negotiation can exclude what the remote already has. On push OK: YAH_SOURCE_CONTEXT_URL=git+https://<fleet-git>/<repo>.git#refs/qed/<run>/<step> plus YAH_SOURCE_GIT_TOKEN (git:read, aud fleet-git/<repo>, TTL = step timeout else 6 h, minted with IssuerKey::Git). On push failure, or YAH_QED_SOURCE_TRANSPORT=tar: publish the tar exactly as today. Worker: node-persistent bare cache repo (host bind in the forge_produced style), under flock: `git fetch fleet +refs/qed/<run>/<step>:refs/remotes/fleet/qed/<run>/<step>` (token via GIT_CONFIG_COUNT env), then `git --work-tree=<step ws> checkout -f <oid> -- .`; prune local qed refs older than 7 d. A failed git fetch fails the step, naming the tar override. Delete the R855-B1 warning on the git path only. No xlb. The tar fallback stays exactly as weak as today (public CDN, admin-pair publisher); closing that is out of scope.")
//! @yah:gotcha("S1: on 2026-10-10 oss/qed/crates/qed/src/build_context.rs was being edited by @Glimmerstone:polaris (session:898072a5, R969). Check camp.roster before touching it. cloud_cheers.rs is shared with F2.")
//! @yah:files(oss/qed/crates/qed/src/build_context.rs)
//! @yah:files(oss/qed/crates/qed/src/runner.rs)
//! @yah:files(app/yah/cli/src/qed_build_context.rs)
//! @yah:files(app/yah/cli/src/cloud_cheers.rs)
//! @yah:next("Tier: Cleric. Leader: F5 has no files in common with F3/F4, so it can run alongside them once F2 lands. It calls R971-F7's git:read mint in cloud_cheers.rs.")
//! @yah:next("OPERATOR CALL 2026-10-10 (form F4371): git transport is the DEFAULT for offloaded steps. On push failure, fall back to tar automatically; YAH_QED_SOURCE_TRANSPORT=tar forces tar. The worker side must not assume Alpine: use the host's git, found on PATH, which must work on glibc distros too (see F2's engine call).")
//! @yah:gotcha("2026-10-10, from the R969 leader @Glimmerstone:polaris: oss/qed/crates/qed/src/runner.rs carries R969-F1..F4's uncommitted hunks, which are in review and form a large share of its +932/-37 diff. They are the step_elections/elect_step_placement memo, the fallback row swap in the main and finally loops, remote_runtime/remote_subprocess_mesh_tags, ElectRequest+resources, and F4's artifact landing near retrieve_remote_artifacts. Edit around them with Edit only and never restore runner.rs from git. build_context.rs and qed_build_context.rs have no R969 hunks. R969-S5/F6/F7 wait for F5 to land.")
//! @yah:next("LEADER 2026-10-10: F2 landed. The fleet-git crate is at oss/yubaba/crates/fleet-git, and the camp client is `yah cloud cheers git --repo <r> -- <git args>` in cloud_cheers.rs (CheersCommands::Git, git_auth_env). Reuse git_auth_env for the coordinator push and the worker-token env; don't write a second one. Build and e2e-test F5 against a LOCAL `fleet-git serve --store memory`. The live checks (yah-cli-release's offloaded musl leg going green, a delta-only second dispatch) need R971-T8's prod deploy. If T8 isn't serving yet when the code is done, use board.handoff for only that live check.")
//! @yah:handoff("Coordinator half landed (uncommitted). NEW oss/qed/crates/qed/src/source_git.rs (mod in lib.rs): snapshot() builds the step's source_context in the bare staging repo .yah/cache/fleet-git/<repo>.git (alternates onto camp .git/objects; temp GIT_INDEX_FILE; ls-tree|update-index, add -A, write-tree, commit-tree). A test asserts the camp's index, refs and status are byte-identical afterwards. push_snapshot() pushes +<commit>:refs/qed/<run>/<step> and mirrors the ref locally. fetch_checkout() is the worker half: under flock, bare cache repo, fetch into refs/remotes/fleet/qed/..., checkout -f <oid> -- . into the workspace; a failure names YAH_QED_SOURCE_TRANSPORT=tar. Both halves prune qed refs older than 7 d and use the host git on PATH. Also adds the SourceGitPublisher trait plus the YAH_SOURCE_GIT_TOKEN / YAH_QED_SOURCE_TRANSPORT consts.")
//! @yah:handoff("runner.rs (Edit only, around R969 hunks): new field and builder with_source_git_publisher. publish_source_context tries git first unless tar is forced. On a git error it emits a step note and falls back to the tar publisher. PublishedSourceContext.key is now Option and the git path discards nothing. Both env sites (local container and remote) set or drop YAH_SOURCE_GIT_TOKEN. The R855-B1 warning is not reached on the git path because it lives only in the tar pack, so build_context.rs was not edited.")
//! @yah:handoff("CLI: FleetGitSourcePublisher in app/yah/cli/src/qed_build_context.rs. Remote comes from YAH_FLEET_GIT_URL, repo from YAH_FLEET_GIT_REPO (default yah). It mints with the existing git_push_token / git_read_token (TTL = step timeout, else 6 h) and passes tokens only via git_auth_env. It is wired in qed.rs and camp.rs with_camp_runner_wiring next to the R2 publisher. In cloud_cheers.rs, git_push_token_with / git_read_token_with / public_half became pub(crate) for the e2e test.")
//! @yah:handoff("DEVIATION FROM W363 s6.1: snapshots are CHAINED, not parentless. Measured: with parentless snapshots, upload-pack re-sent the full tree (7/7 objects) after a one-file edit, because a have on an unrelated root excludes no trees. chain_parent() parents each snapshot on the staging repo's newest refs/qed tip. A chain whose root is older than 6 d restarts parentless, so the 7-day GC can still free old chains. W363 s6 text not updated.")
//! @yah:handoff("Tests: yah-qed lib went from 1081 pass after the module landed to 1084 pass / 0 fail / 1 ignored. New: 7 source_git unit tests, including a file:// round trip asserting the delta is 5 objects or fewer, plus 3 runner tests (git is the default and sets the token; a failed push falls back to tar; forced tar never tries git). yah --lib qed_build_context: 4/4 pass, including the #[ignore]d fleet_git_e2e_untracked_file_and_delta_fetch run with --include-ignored against a local `fleet-git serve --store memory` (oss/yubaba/target/debug/fleet-git) with real minted tokens. It covers the untracked file reaching the checkout, the second fetch moving 5 objects or fewer, and a read token being refused for push. yah --lib cloud_cheers: 10/10. I did not take a pre-edit baseline. Logs: /tmp/r971-f5-{3,4,5,6}.log.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("Live check (needs R971-T8 serving; T8 was still active at handoff): export YAH_FLEET_GIT_URL=http://<fleet-git mesh addr>:4330 on the camp, then run yah-cli-release's offloaded musl leg green on the git transport. Dispatch twice with a one-file edit between and confirm the second fetch's `Total N` (FetchStats.log) shows only the delta. Confirm refs/qed/* is GC'd after 7 d.")
//! @yah:gotcha("Scratch-repo git writes from an agent's own Bash (git add/commit under /tmp/r971-f5) are refused by the camp git-policy hook. The same git operations run fine from inside cargo tests, so debug the git transport through Rust tests.")
//! @yah:handoff("Phase 2, worker wiring, landed (uncommitted). New oss/qed/crates/qed/src/source_fetch.sh is yah-source-fetch, a POSIX sh script and the ONE materializer for both transports. git+<remote>#<ref>: a probe for git, flock on a bare cache repo, a fetch into refs/remotes/fleet/qed/..., checkout into <dest>, and a prune of refs older than 7 d. The token travels via GIT_CONFIG_COUNT/http.extraHeader from YAH_SOURCE_GIT_TOKEN, never argv. If git is missing or the fetch fails, the step fails with a message naming YAH_QED_SOURCE_TRANSPORT=tar, with no fallback. https: curl | tar --no-same-owner. source_git.rs exposes SOURCE_FETCH_SCRIPT and SOURCE_FETCH_ENV=YAH_SOURCE_FETCH. fetch_checkout(cache, context_url, token, ws) now runs that same script in-process, so the tests exercise the real path.")
//! @yah:handoff("runner.rs: YAH_SOURCE_FETCH is injected next to YAH_SOURCE_CONTEXT_URL at both the local-container and the remote env sites (Edit only; R969 hunks untouched). Cache bind: new workload_spec::forge_source_git maps host /var/lib/yah/qed/source-git (under the forge-state root, so yubaba creates it) to /yah/source-git. It is pushed in velveteen-exec/src/remote.rs next to the forge_produced durable mount, for every remote subprocess forge. Local containers and native forges have no bind, so they use a scratch cache, which means a full fetch.")
//! @yah:handoff("Migrated off hand-rolled curl|tar to `sh -c \"$YAH_SOURCE_FETCH\" yah-source-fetch <dest>`: .yah/qed/mesofact-musl.toml (both legs), yah-cli-release.toml, yah-cli-release-windows.toml (WSL sh into $ROOT_L/src), the fleet_portability.rs and runner.rs mesofact fixtures, the remedy text, and app/yah/cli/tests/camp_qed_image_pins.rs, which now asserts YAH_SOURCE_FETCH and no curl. Both musl builder images already apk-install git. W363 s6 now describes the chained snapshots and the fetch path as built.")
//! @yah:handoff("Tests: yah-qed lib 1085/0/1 ignored (baseline 1084, plus a new tar-path script test; the git round trip still moves 5 objects or fewer through the script). velveteen-exec lib 136/0. yah --lib qed_build_context 4/4 including the --include-ignored fleet_git_e2e against oss/yubaba/target/debug/fleet-git, with a real token through the script. yah --lib cloud_cheers 10/10. yah --test main camp_qed_image_pins 5/5. Logs: /tmp/r971-f5b-{2,3,4,5,9}.log. YAH_FLEET_GIT_URL is left unset.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("Live check, needs R971-T8 serving (T8 is in handoff, blocked on R971-B9 as of 2026-10-10): export YAH_FLEET_GIT_URL=http://<fleet-git mesh addr>:4330 on the camp and run yah-cli-release's offloaded musl leg green on the git transport. Dispatch twice with a one-file edit in between, and confirm the second step's yah-source-fetch log shows `Total N` for only the delta. Confirm refs/qed/* is GC'd after 7 d. The worker host must have /var/lib/yah/qed/source-git creatable by yubaba (forge-state root).")
//! @yah:next("LEADER: the code is complete (worker materializer $YAH_SOURCE_FETCH, /yah/source-git bind, pipelines migrated, W363 s6 updated). Only the live check remains. Run it against the image R971-F4 phase 2 deploys (F2+F3+F4), not F2's r971-t8a, so the yah-cli-release leg exercises the durable server. Set YAH_FLEET_GIT_URL only for that check session; persisting it camp-wide is a separate step to confirm with the operator once the check is green.")
//! @yah:soft_depends_on(R971-T8)
//! @yah:soft_depends_on(R971-F4)
//! @yah:handoff("LIVE CHECK, run against fleet-git:r971-f4a at http://100.64.0.3:4330. YAH_FLEET_GIT_URL was set only in the courier's own shell and runs used `--in-process`; it is NOT persisted camp-wide, which awaits an operator call. To avoid yah-cli-release's on_success R2 publish (not authorised), the check ran scratch pipeline .yah/qed/r971-f5-live-check.toml: yah-cli-release's build-x86_64-unknown-linux-musl leg copied verbatim, with a dirty-tolerant stamp and workspace=live. Dispatch 1 (snapshot 5744cc42) offloaded to us-west-002 and checked out over git. It produced /var/lib/yah/qed/produced/9b062b28.../yah-x86_64-unknown-linux-musl.tar.gz, but the coordinator was killed by the harness 30-min background limit before it reported. Dispatch 2 (snapshot 16d6a29f, chained on 5744cc42, one-file edit) landed on us-west-003 with a cold cache and PASSED green on the git transport (log /tmp/r971-final-30.log). Its worker cache was 4320 objects, i.e. 4316 + 4. Dispatch 3 (snapshot 402f9220, second one-file edit) landed on us-west-002, which held snapshot 1. Its fetch added exactly 8 loose objects with the 4316 in-pack unchanged, and `git rev-list --objects 402f9220 --not 5744cc42` = 8 = two snapshots x (commit, root tree, scripts tree, blob). DELTA-ONLY IS PROVEN worker-side. Staging repo: 16d6a29f vs 5744cc42 = 4 objects.")
//! @yah:handoff("Tree anchor at handoff: 2f1470ad404f51379c385724876d6ddc14f8426f — the shared tree as I left it. Diff against it (`git diff 2f1470ad404f51379c385724876d6ddc14f8426f..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
//! @yah:next("Delete the scratch pipeline .yah/qed/r971-f5-live-check.toml (courier-created, no other references). Persisting YAH_FLEET_GIT_URL camp-wide is still an operator call. refs/qed/* 7-day GC is not yet observed: re-check refs/qed on fleet-git and the worker caches after 2026-10-18.")
//! @yah:gotcha("The harness's Bash background limit (30 min) kills an in-process `yah qed run` mid-build. Run the long dispatch with run_in_background and timeout 7200000, or via the daemon with YAH_FLEET_GIT_URL in the daemon's env (operator call). Scratch-repo git fetch/init under /tmp is refused by the gate policy (hard_deny), so measure deltas read-only with `git rev-list --objects A --not B` in .yah/cache/fleet-git/yah.git and the worker caches.")
//! @yah:next("Read /tmp/r971-final-31.log for dispatch 3's final verdict (run in the background as bvu03r4qp; its wait was blocked by two yah-gate timeouts), then board.review F5. Dispatch 2 is already a green run on the git transport, and the delta is already proven.")
//! @yah:handoff("Dispatch 3 (run 24d60fb0, snapshot 402f9220, log /tmp/r971-final-31.log) finished green: both steps passed, run status Success, EXIT=0. Dispatch 2 was green earlier (/tmp/r971-final-30.log). Scratch pipeline .yah/qed/r971-f5-live-check.toml deleted. Persisting YAH_FLEET_GIT_URL camp-wide, which would make git the live default transport, still needs an operator go-ahead; it was set only in the check shell. W363 s1-s7 re-diligenced 2026-10-11: s6 corrected (staging repo and worker fetch use the URL directly, no named remote).")
//! @yah:verify("Dispatch 2 (snapshot 16d6a29f) passed green on the git transport, us-west-003, cold cache.")
//! @yah:verify("Dispatch 3 (snapshot 402f9220) passed green: run 24d60fb0 ended with status Success, EXIT=0, per /tmp/r971-final-31.log.")
//! @yah:verify("Worker cache went from 4316 to 4320 objects across dispatches, i.e. delta only; dispatch 3 fetched 8 loose objects on us-west-002 with the 4316 in-pack unchanged.")
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use crate::runner::RunnerError;
/// Ceiling on the packed context, counted as bytes *read from disk* so the
/// walk aborts early instead of after compressing several GB.
///
/// This exists because the runner's historical default context was the whole
/// camp root, and a camp root contains `target/`. Rather than guess at
/// ignore rules (a half-implemented `.dockerignore` would silently change what
/// gets built), refuse loudly and point at the `context` key — narrowing it is
/// both the fix and the thing the operator wanted anyway.
pub const MAX_CONTEXT_BYTES: u64 = 512 * 1024 * 1024;
/// Ships a build-image step's context to somewhere the *build worker* can
/// fetch it, returning the URL BuildKit should load it from.
#[async_trait]
pub trait BuildContextPublisher: Send + Sync {
/// Upload `tarball` under `key`, returning a URL that GETs those bytes.
///
/// `key` is an opaque, single-use, collision-free stem (the forge id). The
/// implementation owns the bucket, prefix and public host. Single-use
/// matters: a key that has never been requested cannot be serving a
/// negatively-cached 404 from a CDN edge.
async fn publish(&self, key: &str, tarball: Vec<u8>) -> Result<String, RunnerError>;
/// Best-effort delete once the build no longer needs the bytes.
///
/// Returns `()`, not `Result`: the build has already happened by the time
/// this runs, and failing a green build over an undeleted temp object
/// would be strictly worse than leaking it. Implementations log.
async fn discard(&self, key: &str);
}
/// The no-transport default. Every `publish` is a refusal naming what to wire.
///
/// A silent fallback to the bind-mount shape would be the wrong default: it
/// fails inside runc on the worker, minutes later, with a message about a path
/// that means nothing to the reader.
pub struct NoBuildContextPublisher;
#[async_trait]
impl BuildContextPublisher for NoBuildContextPublisher {
async fn publish(&self, _key: &str, _tarball: Vec<u8>) -> Result<String, RunnerError> {
Err(RunnerError::InvalidConfig(
"this step runs on a different host than qed, so the bytes it needs (a \
build-image context, or an offloaded subprocess's `source_context` \
subtrees) have to be fetched by the worker rather than bind-mounted — but \
no build-context publisher is wired into this runner. The `yah` CLI wires \
an R2-backed one on every fleet-capable runner; a bare `yah-qed` embedding \
must supply its own via `PipelineRunner::with_build_context_publisher`."
.into(),
))
}
async fn discard(&self, _key: &str) {}
}
/// Pack `context_dir` into a gzipped tar, appending `extra` entries at the
/// tar root afterwards.
///
/// `extra` lands last on purpose: tar extraction is last-write-wins, so the
/// compiled Dockerfile overrides any same-named file already in the context
/// rather than being shadowed by it. That is the intent — the runner compiled
/// that Dockerfile from the catalog and it is what the step means to build.
///
/// Paths inside the tar are relative to `context_dir`, so the tar root *is*
/// the build context — the shape BuildKit's remote-context unpack expects.
pub fn pack_context(
context_dir: &Path,
extra: &[(String, Vec<u8>)],
) -> Result<Vec<u8>, RunnerError> {
let mut budget = MAX_CONTEXT_BYTES;
let gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
let mut tar = tar::Builder::new(gz);
// Follow symlinks: a context that reaches outside itself through one would
// otherwise arrive on the worker as a dangling link and fail the COPY with
// a file-not-found that names a path only the camp host has.
tar.follow_symlinks(true);
append_dir(&mut tar, context_dir, Path::new(""), &mut budget)?;
for (name, bytes) in extra {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_mtime(0);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.map_err(|e| pack_err(format!("adding {name} to the context tar: {e}")))?;
}
let gz = tar
.into_inner()
.map_err(|e| pack_err(format!("finishing the context tar: {e}")))?;
gz.finish()
.map_err(|e| pack_err(format!("compressing the context tar: {e}")))
}
/// Env var naming the URL a remote subprocess step fetches its source tree
/// from (R560-T8).
///
/// Set by the runner on the offloaded-subprocess path whenever the step
/// declares [`QedStep::source_context`](crate::types::QedStep::source_context);
/// never set otherwise, so a step that does not declare one sees exactly the
/// environment it saw before this existed.
pub const SOURCE_CONTEXT_URL_ENV: &str = "YAH_SOURCE_CONTEXT_URL";
/// Pack the **git-tracked** files under `paths` into one gzipped tar whose
/// entries keep their `camp_root`-relative paths (R560-T8).
///
/// # Why `git ls-files` and not a directory walk
///
/// Because the numbers are not close. `oss/mesofact` is 28 GB on disk and 5 MB
/// tracked — the difference is `target/` and seven `node_modules/`. A walk
/// would trip [`MAX_CONTEXT_BYTES`] long before it reached a source file, and
/// the fix would be to reinvent ignore rules that git already computes
/// correctly for this tree. Reading from the *working tree* rather than from a
/// git object means uncommitted edits travel, which is the behaviour a build
/// dispatched from a live camp has to have.
///
/// # Why the paths stay camp-root-relative
///
/// Unlike a build-image context — where the tar root *is* the context — the
/// consumer here unpacks into an empty directory and expects a repo-shaped
/// tree. `oss/mesofact`'s path deps escape its own workspace into
/// `oss/yah-base` and `oss/cheers` (that is why the image's `build-mesofact.sh`
/// takes a repo root and refuses without both siblings). Flattening to a
/// per-path tar root would break exactly that.
///
/// A tracked path that has been deleted in the working tree is skipped: the
/// tree really does not have it, and the point is to ship the tree as
/// positioned.
///
/// @arch:see(.yah/docs/working/W235-remote-qed.md)
///
pub fn pack_source_context(camp_root: &Path, paths: &[PathBuf]) -> Result<Vec<u8>, RunnerError> {
let rel = source_context_files(camp_root, paths)?;
let mut budget = MAX_CONTEXT_BYTES;
let gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
let mut tar = tar::Builder::new(gz);
tar.follow_symlinks(true);
for entry in &rel {
let abs = camp_root.join(entry);
let meta = match std::fs::metadata(&abs) {
Ok(meta) => meta,
// Tracked but deleted in the working tree, or a dangling symlink.
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(source_err(format!("stat {}: {e}", abs.display()))),
};
if meta.is_dir() {
// A tracked gitlink (submodule) shows up as a path with no file
// behind it. Nothing to ship, and recursing would leave the
// camp-root-relative contract.
continue;
}
let len = meta.len();
budget = budget.checked_sub(len).ok_or_else(|| {
source_err(format!(
"source context exceeds the {} MiB cross-host limit (tripped at {}). \
Narrow `source_context` to the subtrees the build actually compiles.",
MAX_CONTEXT_BYTES / (1024 * 1024),
entry.display(),
))
})?;
let mut file = std::fs::File::open(&abs)
.map_err(|e| source_err(format!("opening {}: {e}", abs.display())))?;
tar.append_file(entry, &mut file).map_err(|e| {
source_err(format!("adding {} to the source tar: {e}", entry.display()))
})?;
}
let gz = tar
.into_inner()
.map_err(|e| source_err(format!("finishing the source tar: {e}")))?;
gz.finish()
.map_err(|e| source_err(format!("compressing the source tar: {e}")))
}
/// The git-tracked files under `paths`, camp-root-relative and sorted.
///
/// Shared by [`pack_source_context`] and [`source_context_fingerprint`] so the
/// two cannot disagree about *which* files a step's source context is. A
/// fingerprint that covered a different set than the pack ships would be a
/// cache key that misses the change it exists to detect.
fn source_context_files(camp_root: &Path, paths: &[PathBuf]) -> Result<Vec<PathBuf>, RunnerError> {
if paths.is_empty() {
return Err(source_err(
"source context requested with no paths".to_string(),
));
}
let mut cmd = std::process::Command::new("git");
cmd.arg("-C").arg(camp_root).args(["ls-files", "-z", "--"]);
for path in paths {
cmd.arg(path);
}
let out = cmd.output().map_err(|e| {
source_err(format!(
"running git ls-files in {}: {e}",
camp_root.display()
))
})?;
if !out.status.success() {
return Err(source_err(format!(
"git ls-files in {} failed: {}",
camp_root.display(),
String::from_utf8_lossy(&out.stderr).trim(),
)));
}
// Deterministic order, for the same reason `append_dir` sorts: two packs of
// an unchanged tree must produce the same bytes. `git ls-files` already
// sorts, but sorting here means that stays true if the source of the list
// ever changes.
let mut rel: Vec<PathBuf> = out
.stdout
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| PathBuf::from(String::from_utf8_lossy(s).into_owned()))
.collect();
rel.sort();
if rel.is_empty() {
return Err(source_err(format!(
"`source_context` matched no git-tracked files under {:?} — the step would \
fetch an empty source tree and fail at the build. Check the paths are \
camp-root-relative and tracked.",
paths,
)));
}
warn_on_untracked_files(camp_root, paths);
Ok(rel)
}
/// Warn (never refuse — R855-B1) when `paths` contains untracked files, since
/// those are silently absent from both [`pack_source_context`]'s tar and
/// [`source_context_fingerprint`]'s cache key. A camp tree is permanently
/// dirty with legitimate untracked files (`target/`, editor scratch, a
/// peer's WIP), so this only fires for untracked files that fall *under a
/// declared source_context path* — the actually-relevant subset — and it
/// never fails the build over it.
fn warn_on_untracked_files(camp_root: &Path, paths: &[PathBuf]) {
let mut cmd = std::process::Command::new("git");
cmd.arg("-C")
.arg(camp_root)
.args(["ls-files", "-z", "--others", "--exclude-standard", "--"]);
for path in paths {
cmd.arg(path);
}
let out = match cmd.output() {
Ok(out) if out.status.success() => out,
_ => return, // Best-effort: the tracked-files call above already
// proved git works here, so a failure here just means
// skip the warning rather than fail the build.
};
let untracked: Vec<PathBuf> = out
.stdout
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| PathBuf::from(String::from_utf8_lossy(s).into_owned()))
.collect();
if untracked.is_empty() {
return;
}
const SHOWN: usize = 10;
let shown: Vec<_> = untracked.iter().take(SHOWN).collect();
let more = untracked.len().saturating_sub(SHOWN);
tracing::warn!(
"source_context under {:?} has {} untracked file(s) that will NOT ship: {:?}{}",
paths,
untracked.len(),
shown,
if more > 0 {
format!(" (+{more} more)")
} else {
String::new()
},
);
}
/// BLAKE3 over the *content* of everything [`pack_source_context`] would ship —
/// the identity of a step's build inputs, as a cache key (R746-F2).
///
/// # Why not just hash the packed tar
///
/// Because the tar is not content-only. [`tar::Builder::append_file`] copies the
/// file's mode, uid/gid and **mtime** into each header, so a `git checkout` that
/// rewrites timestamps without changing a byte produces a different archive. As
/// a transport that is fine; as a cache key it would dispatch an hour-long fleet
/// build for a branch switch that changed nothing. This hashes `(relative path,
/// BLAKE3 of bytes)` pairs instead, so it moves when and only when the source
/// the build compiles moves.
///
/// Executable bit and symlink target are deliberately *not* covered: nothing in
/// a Rust source tree builds differently for them, and including mode would
/// reintroduce a filesystem-dependent key on a tree that gets checked out on
/// both macOS and Linux.
///
/// A tracked path deleted in the working tree is skipped, exactly as the pack
/// skips it — the key describes the tree as positioned.
pub fn source_context_fingerprint(
camp_root: &Path,
paths: &[PathBuf],
) -> Result<String, RunnerError> {
let rel = source_context_files(camp_root, paths)?;
let mut acc = blake3::Hasher::new();
for entry in &rel {
let abs = camp_root.join(entry);
let meta = match std::fs::metadata(&abs) {
Ok(meta) => meta,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(source_err(format!("stat {}: {e}", abs.display()))),
};
// A tracked gitlink (submodule) has no file behind it — same skip the
// pack makes.
if meta.is_dir() {
continue;
}
let bytes = std::fs::read(&abs)
.map_err(|e| source_err(format!("reading {} for fingerprint: {e}", abs.display())))?;
// Length-delimited so no rename can collide with a content change:
// `a/bc` + `d` and `a/b` + `cd` hash differently.
let path_bytes = entry.to_string_lossy();
acc.update(&(path_bytes.len() as u64).to_le_bytes());
acc.update(path_bytes.as_bytes());
acc.update(blake3::hash(&bytes).as_bytes());
}
Ok(acc.finalize().to_hex().to_string())
}
/// Recursive walk. Written by hand rather than with `Builder::append_dir_all`
/// so the byte budget can abort mid-walk — `append_dir_all` on a camp root
/// would happily stream `target/` for minutes before anyone could object.
fn append_dir<W: std::io::Write>(
tar: &mut tar::Builder<W>,
dir: &Path,
prefix: &Path,
budget: &mut u64,
) -> Result<(), RunnerError> {
let entries = std::fs::read_dir(dir)
.map_err(|e| pack_err(format!("reading build context {}: {e}", dir.display())))?;
// Deterministic order: the same context must pack to the same bytes across
// hosts, or a future content-addressed cache key over this tar would miss
// on directory-iteration order alone.
let mut names: Vec<std::ffi::OsString> = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| pack_err(format!("walking {}: {e}", dir.display())))?;
names.push(entry.file_name());
}
names.sort();
for name in names {
let path = dir.join(&name);
let rel = prefix.join(&name);
let meta = std::fs::metadata(&path)
.map_err(|e| pack_err(format!("stat {}: {e}", path.display())))?;
if meta.is_dir() {
append_dir(tar, &path, &rel, budget)?;
continue;
}
let len = meta.len();
*budget = budget
.checked_sub(len)
.ok_or_else(|| RunnerError::StepFailed {
step: "build-image".into(),
msg: format!(
"build context {} exceeds the {} MiB cross-host limit (tripped at {}). \
A remote build ships its context over the network, so the whole \
directory has to travel — set `context = \"<dir>\"` on the step to the \
directory the Dockerfile actually COPYs from instead of the camp root.",
path.display(),
MAX_CONTEXT_BYTES / (1024 * 1024),
rel.display(),
),
})?;
let mut file = std::fs::File::open(&path)
.map_err(|e| pack_err(format!("opening {}: {e}", path.display())))?;
tar.append_file(&rel, &mut file)
.map_err(|e| pack_err(format!("adding {} to the context tar: {e}", rel.display())))?;
}
Ok(())
}
fn pack_err(msg: String) -> RunnerError {
RunnerError::StepFailed {
step: "build-image".into(),
msg,
}
}
fn source_err(msg: String) -> RunnerError {
RunnerError::StepFailed {
step: "source-context".into(),
msg,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn entries_of(tarball: &[u8]) -> Vec<(String, Vec<u8>)> {
let gz = flate2::read::GzDecoder::new(tarball);
let mut archive = tar::Archive::new(gz);
let mut out = Vec::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let name = entry.path().unwrap().to_string_lossy().into_owned();
let mut bytes = Vec::new();
entry.read_to_end(&mut bytes).unwrap();
out.push((name, bytes));
}
out
}
/// The tar root is the context root (no leading directory component), and
/// nested files keep their relative path. BuildKit unpacks the tar *as*
/// the context, so an extra top-level directory would break every COPY.
#[test]
fn packs_relative_to_the_context_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("build-v8.sh"), b"#!/bin/sh\n").unwrap();
std::fs::create_dir(dir.path().join("patches")).unwrap();
std::fs::write(dir.path().join("patches/a.patch"), b"diff\n").unwrap();
let names: Vec<String> = entries_of(&pack_context(dir.path(), &[]).unwrap())
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(names, vec!["build-v8.sh", "patches/a.patch"]);
}
/// R636-B2: the packed tar must carry NO extended attributes.
///
/// This is a load-bearing property, not a nicety. BuildKit unpacks a
/// remote (`--opt context=<url>`) tarball into a snapshot and replays every
/// xattr it finds; a Linux kernel rejects any xattr outside the
/// `user.`/`trusted.`/`security.` namespaces outright. Every source file in
/// this repo carries `com.apple.provenance` on the camp Mac, so a context
/// packed by macOS `tar` — which emits `SCHILY.xattr.*` pax headers —
/// fails the build at `copy /context /` with
/// `lsetxattr <file>: operation not supported`, before the Dockerfile is
/// even read. That failure looked exactly like a BuildKit snapshotter
/// problem and cost this ticket a full diagnostic cycle chasing one.
///
/// `pack_context` is safe because the `tar` crate builds headers from
/// `fs::Metadata` alone and has no xattr support — but "safe by accident of
/// a dependency's feature set" is precisely the kind of property that
/// silently regresses, so pin it.
#[test]
fn packs_without_extended_attributes() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("verify-consumer.sh"), b"#!/bin/sh\n").unwrap();
// Best-effort: set a real xattr so this test has teeth on platforms
// that support one. The assertion below holds either way.
#[cfg(target_os = "macos")]
{
let path = dir.path().join("verify-consumer.sh");
let _ = std::process::Command::new("xattr")
.args(["-w", "com.apple.provenance", "x"])
.arg(&path)
.status();
}
let tarball = pack_context(dir.path(), &[]).unwrap();
let gz = flate2::read::GzDecoder::new(tarball.as_slice());
let mut archive = tar::Archive::new(gz);
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let name = entry.path().unwrap().to_string_lossy().into_owned();
assert!(
!name.starts_with("._"),
"AppleDouble sidecar leaked into the context tar: {name}"
);
let pax: Vec<String> = entry
.pax_extensions()
.unwrap()
.into_iter()
.flatten()
.map(|e| e.unwrap().key().unwrap().to_string())
.collect();
assert!(
!pax.iter().any(|k| k.contains("xattr")),
"{name} carries xattr pax headers {pax:?} — BuildKit will fail \
the remote-context unpack with lsetxattr: operation not supported"
);
}
}
/// The compiled Dockerfile is appended last so it WINS over a same-named
/// file in the context — the runner compiled it from the catalog and that
/// is what the step means to build.
#[test]
fn extra_entries_override_same_named_context_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("x.Dockerfile"), b"FROM stale\n").unwrap();
let tarball = pack_context(
dir.path(),
&[("x.Dockerfile".to_string(), b"FROM compiled\n".to_vec())],
)
.unwrap();
let entries = entries_of(&tarball);
// Both are present; extraction is last-write-wins, so ours must be last.
let last = entries.last().unwrap();
assert_eq!(last.0, "x.Dockerfile");
assert_eq!(last.1, b"FROM compiled\n");
}
/// Entry order is stable across runs regardless of readdir order.
#[test]
fn packs_in_sorted_order() {
let dir = tempfile::tempdir().unwrap();
for name in ["zeta", "alpha", "mid"] {
std::fs::write(dir.path().join(name), name.as_bytes()).unwrap();
}
let names: Vec<String> = entries_of(&pack_context(dir.path(), &[]).unwrap())
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(names, vec!["alpha", "mid", "zeta"]);
}
/// The size ceiling aborts the walk and the message names both the file it
/// tripped on and the knob that fixes it — an operator who points a remote
/// build at the camp root must not just see "too big".
#[test]
fn oversized_context_is_refused_with_an_actionable_message() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("big.bin"), vec![0u8; 4096]).unwrap();
// Re-run the budget logic at a size the fixture can trip without
// writing half a gigabyte: shrink by packing twice the file size away.
let mut budget = 1024u64;
let err = append_dir(
&mut tar::Builder::new(Vec::new()),
dir.path(),
Path::new(""),
&mut budget,
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("big.bin"), "must name the file: {msg}");
assert!(msg.contains("context ="), "must name the fix: {msg}");
}
// ── pack_source_context (R560-T8) ────────────────────────────────────────
/// A throwaway git repo with `tracked/` committed and `ignored/` matching
/// `.gitignore` — the miniature of the real shape (`oss/mesofact` at 5 MB
/// tracked inside 28 GB of `target/` + `node_modules/`).
fn source_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let git = |args: &[&str]| {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.output()
.unwrap();
assert!(out.status.success(), "git {args:?}: {out:?}");
};
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
std::fs::create_dir_all(root.join("pkg/src")).unwrap();
std::fs::create_dir_all(root.join("pkg/target/debug")).unwrap();
std::fs::create_dir_all(root.join("sibling/src")).unwrap();
std::fs::create_dir_all(root.join("elsewhere")).unwrap();
std::fs::write(root.join(".gitignore"), "target/\n").unwrap();
std::fs::write(root.join("pkg/src/lib.rs"), b"// lib\n").unwrap();
std::fs::write(root.join("pkg/Cargo.toml"), b"[package]\n").unwrap();
std::fs::write(root.join("pkg/target/debug/huge.bin"), vec![0u8; 4096]).unwrap();
std::fs::write(root.join("sibling/src/dep.rs"), b"// dep\n").unwrap();
std::fs::write(root.join("elsewhere/nope.rs"), b"// nope\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "init"]);
dir
}
/// The two properties the whole transport rests on: entries keep their
/// **camp-root-relative** paths (so an escaping path dep still resolves
/// after unpacking), and only **git-tracked** files travel (so `target/`
/// cannot blow the size ceiling before a single source file ships).
///
/// The path shape is the one that differs from `pack_context`, where the
/// tar root *is* the context. Getting it wrong here produces a tar that
/// unpacks to `src/lib.rs` instead of `pkg/src/lib.rs`, and cargo then
/// fails on a missing workspace member rather than on anything that names
/// the real cause.
#[test]
fn source_context_ships_tracked_files_at_camp_relative_paths() {
let repo = source_repo();
let tarball = pack_source_context(
repo.path(),
&[PathBuf::from("pkg"), PathBuf::from("sibling")],
)
.unwrap();
let names: Vec<String> = entries_of(&tarball).into_iter().map(|(n, _)| n).collect();
assert_eq!(
names,
vec!["pkg/Cargo.toml", "pkg/src/lib.rs", "sibling/src/dep.rs"],
"camp-relative paths, sorted, tracked-only",
);
assert!(
!names.iter().any(|n| n.contains("target/")),
"an ignored build dir must not travel: {names:?}",
);
assert!(
!names.iter().any(|n| n.starts_with("elsewhere/")),
"only the named subtrees travel: {names:?}",
);
}
/// Two packs of an unchanged tree produce identical bytes. A future
/// content-addressed cache over this tar would otherwise miss on
/// directory-iteration order alone — the same property `pack_context`'s
/// sort exists for.
#[test]
fn source_context_packs_deterministically() {
let repo = source_repo();
let paths = [PathBuf::from("pkg"), PathBuf::from("sibling")];
assert_eq!(
pack_source_context(repo.path(), &paths).unwrap(),
pack_source_context(repo.path(), &paths).unwrap(),
);
}
/// A tracked file deleted in the working tree is skipped rather than
/// erroring: the tree really does not have it, and the point of reading the
/// working tree instead of a git object is to ship the tree as positioned.
#[test]
fn source_context_skips_files_deleted_in_the_working_tree() {
let repo = source_repo();
std::fs::remove_file(repo.path().join("pkg/src/lib.rs")).unwrap();
let names: Vec<String> =
entries_of(&pack_source_context(repo.path(), &[PathBuf::from("pkg")]).unwrap())
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(names, vec!["pkg/Cargo.toml"]);
}
/// A path that matches nothing tracked is refused at pack time, not
/// discovered on the worker. The failure mode it prevents is the expensive
/// one: dispatch, a ~2.5 GB image pull, a container start, and then cargo
/// failing on an empty directory with a message about a workspace root.
#[test]
fn source_context_matching_nothing_is_refused_with_an_actionable_message() {
let repo = source_repo();
let err = pack_source_context(repo.path(), &[PathBuf::from("oss/typo")]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("no git-tracked files"), "{msg}");
assert!(msg.contains("camp-root-relative"), "{msg}");
}
/// The fingerprint is content-only: touching every file (what a `git
/// checkout` or a `cargo` pass does to mtimes) must not move it. This is the
/// whole reason it is not `blake3(pack_source_context(..))` — the tar
/// carries mtimes in its headers, so hashing it would rebuild on a branch
/// switch that changed nothing.
#[test]
fn fingerprint_ignores_mtime() {
let repo = source_repo();
let paths = [PathBuf::from("pkg"), PathBuf::from("sibling")];
let before = source_context_fingerprint(repo.path(), &paths).unwrap();
let file = repo.path().join("pkg/src/lib.rs");
let content = std::fs::read(&file).unwrap();
std::fs::remove_file(&file).unwrap();
std::fs::write(&file, &content).unwrap();
assert_eq!(
before,
source_context_fingerprint(repo.path(), &paths).unwrap()
);
// …and the packed tar really does move, which is the thing being
// guarded against rather than assumed.
assert_ne!(
before,
blake3::hash(&pack_source_context(repo.path(), &paths).unwrap())
.to_hex()
.to_string(),
);
}
/// One changed byte in one tracked file moves the fingerprint — the
/// "the Rust side actually changed" signal the build-dispatch arm keys on.
#[test]
fn fingerprint_moves_on_a_content_change() {
let repo = source_repo();
let paths = [PathBuf::from("pkg")];
let before = source_context_fingerprint(repo.path(), &paths).unwrap();
std::fs::write(repo.path().join("pkg/src/lib.rs"), b"pub fn f() {}\n").unwrap();
assert_ne!(
before,
source_context_fingerprint(repo.path(), &paths).unwrap()
);
}
/// An untracked file does not move it. Everything under `oss/mesofact`'s
/// 28 GB of `target/` and `node_modules/` is untracked; a fingerprint that
/// moved with them would never hit.
#[test]
fn fingerprint_ignores_untracked_files() {
let repo = source_repo();
let paths = [PathBuf::from("pkg")];
let before = source_context_fingerprint(repo.path(), &paths).unwrap();
std::fs::create_dir_all(repo.path().join("pkg/target")).unwrap();
std::fs::write(repo.path().join("pkg/target/junk.bin"), b"\x00\x01").unwrap();
assert_eq!(
before,
source_context_fingerprint(repo.path(), &paths).unwrap()
);
}
/// Same refusal as the pack, from the same enumeration — a typo'd subtree
/// must not silently fingerprint as "no inputs" and then reuse a stale
/// binary forever.
#[test]
fn fingerprint_matching_nothing_is_refused() {
let repo = source_repo();
let err =
source_context_fingerprint(repo.path(), &[PathBuf::from("oss/typo")]).unwrap_err();
assert!(err.to_string().contains("no git-tracked files"), "{err}");
}
/// A minimal `tracing::Subscriber` that records event messages, so
/// `warn_on_untracked_files` can be asserted on without a logging
/// dependency the crate doesn't otherwise need.
#[derive(Default)]
struct CapturingSubscriber {
events: std::sync::Mutex<Vec<String>>,
}
impl tracing::Subscriber for CapturingSubscriber {
fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
struct Visitor(String);
impl tracing::field::Visit for Visitor {
fn record_debug(
&mut self,
field: &tracing::field::Field,
value: &dyn std::fmt::Debug,
) {
if field.name() == "message" {
self.0 = format!("{value:?}");
}
}
}
let mut visitor = Visitor(String::new());
event.record(&mut visitor);
self.events.lock().unwrap().push(visitor.0);
}
fn enter(&self, _: &tracing::span::Id) {}
fn exit(&self, _: &tracing::span::Id) {}
}
/// R855-B1: an untracked file under a declared `source_context` path is
/// silently absent from both the tar and the fingerprint (by design —
/// only tracked files travel), so the one local signal an author gets is
/// this warning. It must name the file and must not fail the pack.
#[test]
fn warns_on_untracked_file_in_declared_subtree_but_still_packs() {
let repo = source_repo();
std::fs::write(repo.path().join("pkg/src/new_module.rs"), b"// new\n").unwrap();
let subscriber = std::sync::Arc::new(CapturingSubscriber::default());
let tarball = tracing::subscriber::with_default(subscriber.clone(), || {
pack_source_context(repo.path(), &[PathBuf::from("pkg")]).unwrap()
});
let names: Vec<String> = entries_of(&tarball).into_iter().map(|(n, _)| n).collect();
assert!(
!names.contains(&"pkg/src/new_module.rs".to_string()),
"untracked file must not ship: {names:?}"
);
let events = subscriber.events.lock().unwrap();
assert!(
events.iter().any(|e| e.contains("new_module.rs")),
"must warn naming the untracked file: {events:?}"
);
}
/// A file outside every declared `source_context` path is not this step's
/// business — warning about it would be noise for e.g. every peer's WIP
/// elsewhere in a permanently-dirty camp tree.
#[test]
fn does_not_warn_about_untracked_files_outside_the_declared_paths() {
let repo = source_repo();
std::fs::write(repo.path().join("elsewhere/scratch.rs"), b"// scratch\n").unwrap();
let subscriber = std::sync::Arc::new(CapturingSubscriber::default());
tracing::subscriber::with_default(subscriber.clone(), || {
pack_source_context(repo.path(), &[PathBuf::from("pkg")]).unwrap()
});
let events = subscriber.events.lock().unwrap();
assert!(
events.is_empty(),
"must not warn about files outside the declared paths: {events:?}"
);
}
/// The unwired default refuses rather than silently falling back to the
/// bind-mount shape that fails minutes later inside runc.
#[tokio::test]
async fn no_publisher_refuses_with_the_wiring_instruction() {
let err = NoBuildContextPublisher
.publish("k", vec![])
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("with_build_context_publisher"), "{msg}");
}
}