Skip to content

github-backup(inventory): a null repositoryOwner is silently read as 'this owner has no repos' — reconcile drops the owner's mirrors from plan.json and MANIFEST.json and exits 0 #3229

Closed

Description

@dezren39

github-backup(inventory): a null repositoryOwner is silently read as "this owner has no repos" — reconcile then drops the owner's mirrors from plan.json and MANIFEST.json and exits 0

Evidence (verified this run, 2026-10-31)

infra/github-backup/bin/ghb_common.py, inventory_owner():

data = json.loads(r.stdout)
conn = (data.get("data", {}).get("repositoryOwner") or {}).get("repositories")
if conn is None:
    return repos

Any successful (gh exit 0) GraphQL response whose repositoryOwner is null — the owner was renamed, the token lost visibility of the org (e.g. SAML enforcement, the exact failure mode config.json already records as a GAP for vertexinc), or the org is otherwise not visible — returns an empty repo list for that owner, indistinguishable from an owner that genuinely has zero repositories. The errors channel in inventory() is never populated for this case.

Laptop probe (real ghb_common.py from main 7d2f1f77, sha256 2eb427df… matching the sandbox clone; run() stubbed to return canned GraphQL payloads; probe dir ~/git/fleet/probe-2026-10-31-laneA-ghb-inventory/ on the laptop):

control-visible:   repos=['some-owner/repo1'] errors={}
null-owner:        repos=[] errors={}                      <- the defect
gh-failure:        repos=[] errors={'some-owner': 'gh api graphql failed for some-owner: boom'}
null-repositories: repos=[] errors={}                      <- same defect, repositories field null

The control cases behave: a visible owner inventories normally, and a gh CLI failure raises and is recorded per-owner. Only the null-owner/null-repositories shapes are swallowed.

Why it matters

reconcile.py is the system's correctness floor (its own docstring: inventory errors per owner are reported, and "Exit code is non-zero only on owner-level inventory failure, so the timer unit surfaces it"). With this defect, one invisible owner causes the weekly reconcile to:

  1. overwrite plan.json with none of that owner's repos (they surface only as "no longer planned (mirrors kept on disk)", which reads as an intentional removal);
  2. regenerate MANIFEST.json from the shrunken plan — manifest.py iterates the plan only — so the disaster-recovery source of truth silently stops recording mirrors that still exist on disk;
  3. exit 0 (return 1 if errors else 0, and errors is empty), so the systemd unit reports success.

A backup system whose inventory layer cannot distinguish "owner has nothing" from "owner could not be seen" will eventually un-plan a live part of the estate and call it success.

Distinct from: #3219 (the drainer's ack path), #3215 (the umbrella system issue, which tracks the system as a whole, not this defect).

Acceptance criteria

  • inventory_owner() treats a null/missing repositoryOwner (and a null/missing repositories connection) in an otherwise successful GraphQL response as an owner-level error: it raises (or otherwise records an error for that owner in inventory()), naming the owner and the shape received.
  • A legitimately empty owner (connection present, zero nodes, no next page) still inventories as zero repos with no error.
  • Reconcile surfaces that failure the way its docstring promises: the owner appears in the report's inventory-errors section and the run exits non-zero, without overwriting plan.json in a way that drops the failed owner's previously planned repos (or, if plan.json is still written, the report makes the drop's cause unmistakable).
  • Regression tests cover: null repositoryOwner, null repositories, a GraphQL response carrying a top-level errors field, and the empty-but-visible owner control.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/github-backupinfra/github-backup estate mirror system
    effort/sSmall: expected under 2 hours of work
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. added
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 31, 2026
  4. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: #3215 (github-backup estate mirror system — the parent system this inventory feeds), #3219 and #3230 (sibling github-backup / cold-store verification gaps filed the same day).

    Notes: Verified live by the filer with a laptop probe against real ghb_common.py (main 7d2f1f77): the null-owner and null-repositories shapes return repos=[] with an empty errors channel, indistinguishable from a genuinely empty owner, while a hard gh failure is recorded per-owner. The issue already carries acceptance criteria in its body (probe cases), so no body change was made.

    Risks: This is the fleet's backup correctness floor: a silently-dropped owner loses its mirrors from plan.json/MANIFEST.json with exit 0 — a data-loss-adjacent failure that only surfaces at restore time. Priority/normal stands, but the fix should land before the next reconcile cycle is trusted.

    Suggested approach: Treat null repositoryOwner / null repositories as a per-owner inventory error (populate the existing errors channel, non-zero overall result), and pin the probe's four cases (visible / null-owner / gh-failure / null-repositories) as tests.

    — fleet monitor intake, 2026-10-31 fleet-monitor run (17:12 CDT)

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 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 on ok; the chronic alarm is a streak of 3 consecutive unavailable days (fail, timeout and missing all count); generated state is written atomically only through ghb_common helpers; 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.

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

Metadata

Metadata

Assignees

Labels

app/github-backupinfra/github-backup estate mirror systemeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions