Repository navigation
Description
Problem
infra/github-backup/bin/ghb_common.py:34-38 — the one check every part of the github-backup system uses to decide "this mirror exists and is good":
def valid_mirror(path):
if not os.path.isdir(path):
return False
r = run(["git", "-C", path, "rev-parse", "--is-bare-repository"])
return r.returncode == 0 and r.stdout.strip() == "true"--is-bare-repository is true from the moment git clone --mirror creates the destination skeleton — before any ref or object has been transferred. A clone killed in that window (the driver's own TIMEOUT = 5400 expiry in clone_all.py, an interrupt, a laptop sleep/reboot during the initial estate clone) leaves a bare directory with zero refs and zero objects that passes valid_mirror everywhere it is used:
clone_all.py:44-46—clone_onereturns{"status": "ok", "note": "already-present"}without cloning;clone_all.py:71+ theclone-status.jsonlok_prevreplay do the same on later runs, so the repo is counted in theDONE ok=…tally and never cloned.manifest.py:40—build()reports the empty shell as"status": "mirrored"in MANIFEST.json, the file the runbook calls the disaster-recovery source of truth, withhead: nullas the only tell.fetch_mirrors.py:22— the daily pass treats it as present rather thanmissing(a later successful fetch would heal the data, but until then every status surface claims coverage that does not exist).
This is the mirror image of #3233: there, a partial leftover that is not a bare repo wedges every rerun; here, a partial leftover that is a bare skeleton is certified as a verified mirror. Both come from the same root: nothing ever checks that a "valid" mirror can resolve a single ref.
Evidence (verified this run, main 357ec7f)
Laptop probe ~/git/fleet/probe-2026-10-31-laneA-validmirror/ (on the laptop), driving the real ghb_common.py / manifest.py (sha256 2eb427df… / a489798e…, identical in the sandbox clone, the laptop clone, and the probe copies):
- Skeleton control (
git init --bare, the exact state a mirror clone creates before transport):valid_mirror(...) = True;git rev-parse --verify HEADexits 1; 0 refs. - Real killed clone:
git clone --mirror file:///home/user/git/code(~1 GB.git), killed with SIGKILL the moment--is-bare-repositoryflipped totrue: destination remains,valid_mirror(...) = True,rev-parse --verify HEADexits 1, 0 refs,count-objectsall zero. - Manifest consequence:
manifest.build()over a one-repo plan whose destination is the skeleton from (1) emits"status": "mirrored",head: null, and totals{'mirrored': 1, 'missing': 0}. - Control: a complete mirror clone of a small repo —
valid_mirror = Trueandrev-parse --verify HEADexits 0 with the commit SHA, so a strengthened check keeps the good case green.
Why it matters
A backup system's worst failure is reporting coverage it does not have. Today an interrupted clone of a large repo — exactly the repos most likely to hit the 90-minute timeout or an interrupted laptop session — can be recorded as mirrored in clone-status.jsonl and in MANIFEST.json while holding no objects at all, and no pass will ever re-clone it: clone_all skips it as already-present, and reconcile.py only invokes the clone pass for mirrors it believes are missing. The gap is silent and permanent until a human inspects the tree by hand.
Acceptance criteria
- Mirror validity requires more than bareness: at minimum the mirror's HEAD (or its default-branch ref) resolves to a commit — e.g.
git rev-parse --verify -q HEADsucceeds — in addition to--is-bare-repository. - A bare skeleton with no refs/objects is treated as not-yet-mirrored by every consumer:
clone_allclones (or resumes into) it instead of recordingalready-present/ok,manifest.buildreports itmissing(notmirrored), andfetch_mirrorsreports itmissing. - The legitimately-empty-repo case is handled deliberately, not by accident: a mirror whose upstream repo has zero commits (unborn HEAD by design — cf.
build_plan'sskip_zero_commit_reposhandling in ghb_common.py) is either still accepted via an explicit emptiness signal or documented as out of scope, with a test pinning whichever behaviour is chosen. - Regression tests cover: (a) bare skeleton with 0 refs → invalid, (b) complete mirror → valid, (c) non-bare partial leftover (the github-backup(clone_all): a clone killed mid-flight leaves a partial destination every rerun rejects — the 'resumable' driver never resumes that repo #3233 shape) → still invalid, and fail on the current code for (a).
- The fix composes with github-backup(clone_all): a clone killed mid-flight leaves a partial destination every rerun rejects — the 'resumable' driver never resumes that repo #3233's cleanup: a skeleton cleared/re-cloned under that fix must end in a state the strengthened check accepts only once refs actually exist.
Dedup (this run): title/keyword searches for valid_mirror, clone_all, "partial mirror clone interrupted" return only #3233 (filed minutes ago by the sibling lane — the non-bare leftover wedge; its evidence seeds a non-bare partial dir and its acceptance criteria assume validity fails there) and the umbrella #3215. No issue covers the bare-skeleton false positive or valid_mirror's bareness-only test. Related: #3219 (drain ack path), #3229 (inventory null owner).
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Design-architect pass 29 (2026-10-31) — agent note: this issue is ADOPTED as a phase of design #3282 ("a mirror is what it can prove — one validity, outcome and alarm contract for the github-backup estate mirror", design doc: files/designs/code-github-backup-mirror-contract.md in the keep-the-four-repos-healthy goal workspace), linked as a sub-issue of #3282 and labelled source/design-architect. Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead. Phase mapping: Phase 1 (validity & destinations) = #3238 + #3233; Phase 2 (recorded outcomes & the alarm) = #3250 + #3219 + #3270 + #3257; Phase 3 (durable state & plan retention) = #3240 + #3229 + closing audit #3283 (gated on Phases 1–2). The design records the pins that span the phases: validity = bare + HEAD resolves, with no emptiness exception inside the plan (build_plan excludes zero-commit repos); destinations are absent / valid / skeleton (remove + clone) / debris (quarantine to
.partial-<timestamp>+ clone); every fetch attempt ends in one recorded outcome shared by the daily loop and the drainer, and the drainer acks only onok; the chronic alarm is a streak of 3 consecutive unavailable days (fail, timeout and missing all count); generated state is written atomically only throughghb_commonhelpers; reconcile retains a failed owner's previously planned entries. Where an origin's acceptance left a choice open (the streak-vs-window rule, missing-counts-vs-immediate, report-vs-retain, skeleton handling), the design doc records the decision — triage follows the design's pin, not the open alternative. Each origin keeps its own acceptance criteria and its already-queued triage packet — no new packet was created for it this pass.7 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1 — state=picked_up; task_id=5684; worker=repo-worker-code; issue=#3238. Fresh claim with no prior progress. Next: revalidate issue state, hard-stop labels, and matching open PRs before creating/resuming the task worktree.
dezren39 commentedon Nov 2, 2026
agent-note:v1 — state=blocked; task_id=5684; worker=repo-worker-code; issue=#3238. Queue task blocked as packet_defect. The issue’s triage completion note routes the fix as task #5685, but this atomic claim is #5684 for the same issue/scope; the canonical queue row cannot be established safely. No source files were touched. Resume after task-ID/dedupe reconciliation. Empty worktree/branch created at /home/user/git/fleet/5684 and left untouched.
developing-today commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 5684 (small_fix, blocked, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03