fix(retire): finish hello-lib, and stop rebuild resurrecting pipelines #595

Merged
binjovi-bot merged 2 commits from fix/retire-hello-lib-residue into trunk 2026-09-10 19:27:17 +00:00
Owner

Two retirements were left half done, and both failed the same structural way: under prune: false, removing a declaration stops Flux managing an object — it does not delete it.

hello-lib

ad45bd04 deleted gitmirrors/hello-lib.yaml and stopped there. Both CRs carry Retain, so nothing was ever going to clean them up. Four days later the residue is:

ForgejoRepository/hello-lib   Retain, status.repositoryID 391
ForgejoPushMirror/hello-lib   Retain, failing every reconcile since 01:14:43 with
                              "fork/exec /usr/bin/git: no such file or directory"
sean/hello-lib                empty: true, size 0, ZERO refs

None of it is declared anywhere in this repository. The deleted file's own header said what to do instead: "Retiring a repo is still a deliberate act: set deletionPolicy: Delete first, per README.md."

The fence, and why it differs from the pipelines tombstone

The pipelines Job refuses if deletionPolicy says Delete, because archiving kept the code. Here the intent is destruction, so the safety property cannot be "never destroy" — it has to be "destroy only what is proven empty."

The Job reads the repository from Forgejo and refuses unless it is empty, zero-size, and still repository id 391. Pinning the id matters: a name can be recycled, and a future sean/hello-lib holding real code must not be deleted by a stale Job. That proof is exactly the check ad45bd04 never had.

It needs no credential — the repository is public, so the fence is an anonymous read against the in-cluster Forgejo service.

Order is enforced: mirror released first (it is the one still trying to write to Codeberg), emptiness proven, only then the policy flip, then the effect verified as a 404.

pipelines — a live bug

d6639f8a archived it in six files: the two gitmirror CRs and nothing else. It is still in repository-bootstrap.json, so make rebuild still re-creates and re-imports sean/pipelines from Codeberg — precisely what that commit said archiving would stop. It is also still in the management grants, so the bootstrap re-grants binjovi-operator-bot admin on an archived repo every cold rebuild.

Both entries go. Seed 42 → 41, completions with it, grants 43 → 42.

repository-retirement/ is durable

Unlike pipelines-retirement/, which is documented as delete-the-whole-thing-when-drained. Retirement recurs, and every one leaves orphans that only a Job like this can release. Individual tombstones are still one-shot — "AFTER IT RUNS this file is one git rm" — but the directory outlives them.

Verification

tests/hello-lib-retired.sh pins the emptiness fence, the id pin, the pinned RBAC and its exact verbs, the release order, and that the policy flip comes after the proof. Five controls confirmed red against the real manifest:

  • emptiness fence removed
  • repository id unpinned
  • RBAC widened to a second repository
  • verbs widened
  • policy flip moved ahead of the proof

The guard also caught a bug in itself during development: a loose deletionPolicy":"Delete" pattern matched the mirror's refuse-check rather than the patch body, so the ordering assertion fired on correct code. Tightened to the patch body.

Passing: every-guard-has-a-caller, hello-lib-retired, pipelines-archived, binjovi-repository-bootstrap, binjovi-collaborator-bootstrap, binjovi-service-handoff, gitmirror-fleet, retirement-jobs-are-replaceable, binjovi.

Pre-flight recorded for the destructive step: sean/hello-lib confirmed empty: true, size 0, git ls-remote returns nothing, id 391 matching the CR's status.repositoryID. 45 repositories before the change; expect exactly 44 after and no other change.

Not in this PR

pipelines still appears in five RBAC rules and five binjovi binding modules, and four guards actively require itbinjovi-service-handoff.sh:10,14, binjovi-active-webhook-handoff.sh:12, forgejo_service_catalog_test.exs:6-9. Unwinding guards written to prevent that edit is its own deliberate change, not something to slip into a cleanup.

https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8

Two retirements were left half done, and both failed the same structural way: under `prune: false`, removing a declaration stops Flux **managing** an object — it does not delete it. ## hello-lib `ad45bd04` deleted `gitmirrors/hello-lib.yaml` and stopped there. Both CRs carry `Retain`, so nothing was ever going to clean them up. Four days later the residue is: ``` ForgejoRepository/hello-lib Retain, status.repositoryID 391 ForgejoPushMirror/hello-lib Retain, failing every reconcile since 01:14:43 with "fork/exec /usr/bin/git: no such file or directory" sean/hello-lib empty: true, size 0, ZERO refs ``` None of it is declared anywhere in this repository. The deleted file's own header said what to do instead: *"Retiring a repo is still a deliberate act: set deletionPolicy: Delete first, per README.md."* ### The fence, and why it differs from the pipelines tombstone The pipelines Job **refuses** if `deletionPolicy` says `Delete`, because archiving kept the code. Here the intent is destruction, so the safety property cannot be "never destroy" — it has to be **"destroy only what is proven empty."** The Job reads the repository from Forgejo and refuses unless it is `empty`, zero-size, **and still repository id 391**. Pinning the id matters: a name can be recycled, and a future `sean/hello-lib` holding real code must not be deleted by a stale Job. That proof is exactly the check `ad45bd04` never had. It needs **no credential** — the repository is public, so the fence is an anonymous read against the in-cluster Forgejo service. Order is enforced: mirror released first (it is the one still trying to write to Codeberg), emptiness proven, only then the policy flip, then the effect verified as a 404. ## pipelines — a live bug `d6639f8a` archived it in six files: the two gitmirror CRs and nothing else. It is still in `repository-bootstrap.json`, so **`make rebuild` still re-creates and re-imports `sean/pipelines` from Codeberg** — precisely what that commit said archiving would stop. It is also still in the management grants, so the bootstrap re-grants `binjovi-operator-bot` admin on an archived repo every cold rebuild. Both entries go. Seed 42 → 41, `completions` with it, grants 43 → 42. ## `repository-retirement/` is durable Unlike `pipelines-retirement/`, which is documented as delete-the-whole-thing-when-drained. Retirement recurs, and every one leaves orphans that only a Job like this can release. Individual tombstones are still one-shot — *"AFTER IT RUNS this file is one `git rm`"* — but the directory outlives them. ## Verification `tests/hello-lib-retired.sh` pins the emptiness fence, the id pin, the pinned RBAC and its exact verbs, the release order, and that the policy flip comes **after** the proof. Five controls confirmed red against the real manifest: - emptiness fence removed - repository id unpinned - RBAC widened to a second repository - verbs widened - policy flip moved ahead of the proof The guard also caught a bug in itself during development: a loose `deletionPolicy":"Delete"` pattern matched the mirror's refuse-check rather than the patch body, so the ordering assertion fired on correct code. Tightened to the patch body. Passing: `every-guard-has-a-caller`, `hello-lib-retired`, `pipelines-archived`, `binjovi-repository-bootstrap`, `binjovi-collaborator-bootstrap`, `binjovi-service-handoff`, `gitmirror-fleet`, `retirement-jobs-are-replaceable`, `binjovi`. **Pre-flight recorded for the destructive step**: `sean/hello-lib` confirmed `empty: true`, size 0, `git ls-remote` returns nothing, id 391 matching the CR's `status.repositoryID`. 45 repositories before the change; expect exactly 44 after and no other change. ## Not in this PR `pipelines` still appears in five RBAC rules and five binjovi binding modules, and **four guards actively require it** — `binjovi-service-handoff.sh:10,14`, `binjovi-active-webhook-handoff.sh:12`, `forgejo_service_catalog_test.exs:6-9`. Unwinding guards written to prevent that edit is its own deliberate change, not something to slip into a cleanup. https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8
fix(retire): finish hello-lib, and stop rebuild resurrecting pipelines
Some checks failed
binjovi/ci Binjovi failed the frozen plan
3ea1019d2d
Two retirements were left half done. Both failed the same way, and the way is
structural: under `prune: false` removing a declaration stops Flux MANAGING an
object, it does not delete it.

HELLO-LIB. `ad45bd04` deleted `gitmirrors/hello-lib.yaml` and stopped. Both CRs
carry `Retain`, so nothing was ever going to clean them up. Four days later the
residue is a live `ForgejoRepository/hello-lib`, a live
`ForgejoPushMirror/hello-lib` failing every reconcile with
`fork/exec /usr/bin/git: no such file or directory`, and an empty
`sean/hello-lib` that no declaration mentions. The deleted file's own header said
what to do instead: "set deletionPolicy: Delete first, per README.md."

A tombstone Job finishes it, following the pipelines gitmirror tombstone for
shape. It differs in one deliberate way: pipelines KEPT its code, so that Job
refuses if the policy says Delete. Here the intent is destruction, so the safety
property cannot be "never destroy" -- it has to be "destroy only what is PROVEN
empty". The Job reads the repository from Forgejo and refuses unless it is empty,
zero-size, and still repository id 391. Pinning the id matters because a name can
be recycled and a future `sean/hello-lib` holding real code must not be deleted
by a stale Job. That proof is the check `ad45bd04` never had. It needs no
credential: the repository is public, so the fence is an anonymous read.

PIPELINES. `d6639f8a` archived it in six files -- the two gitmirror CRs and
nothing else. It is still in `repository-bootstrap.json`, so `make rebuild` still
re-creates and re-imports `sean/pipelines` from codeberg, which is exactly what
that commit said archiving would stop. It is also still in the management grants,
so the bootstrap re-grants `binjovi-operator-bot` admin on it every cold rebuild.
Both entries go; the seed drops to 41 and the Job's completions with it.

`repository-retirement/` is DURABLE, unlike `pipelines-retirement/` which is
documented as delete-when-drained. Retirement recurs, and every one of them
leaves orphans that only a Job like this can release.

`tests/hello-lib-retired.sh` pins the emptiness fence, the id pin, the pinned
RBAC and its exact verbs, the release order, and that the policy flip comes AFTER
the proof. Five controls confirmed red: fence removed, id unpinned, RBAC widened,
verbs widened, and the flip moved ahead of the proof.

The RBAC rules and binjovi bindings still name `pipelines`, and four guards
REQUIRE that residue. Unwinding those is its own deliberate change.

Claude-Session: https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8
sean force-pushed fix/retire-hello-lib-residue from 3ea1019d2d
Some checks failed
binjovi/ci Binjovi failed the frozen plan
to 2c7f70c224
Some checks failed
binjovi/ci Binjovi failed the frozen plan
2026-09-10 19:07:47 +00:00
Compare
fix(retire): bind the hello-lib fence to the CR, not to a constant id
All checks were successful
binjovi/ci Binjovi completed the frozen plan
2ad108b672
The tombstone pinned `repo_id=391`, to stop a stale Job deleting a future
`sean/hello-lib` that holds real code. That property is correct. The constant is
the wrong way to hold it.

The orphan is not inert. Two loops rebuild `sean/hello-lib` from a source that
does not exist. The orphaned `ForgejoRepository` imports it again, and the
`forgejo-mirror-recovery` CronJob clones it again every 30 minutes. Both read
codeberg `someara/hello-lib`, which answers 404. The id moved from 391 to 392
while this branch was open. The constant thus named the wrong repository, the Job
would refuse the retirement that it exists to do, and the easy repair would be to
increase the number -- which turns a fence into a formality.

The Job now reads `status.repositoryID` from `ForgejoRepository/hello-lib`. The
repository it proves empty must carry that same id. This is the same property,
and it stays true when the id moves: delete the repository this CR owns, and
nothing else. A CR that reports no id deletes nothing. Recovery enumerates
repository CRs, so deleting the CR stops that loop too.

The repository CR is now read BEFORE the fence, and released after it, so the
identity is known when the proof runs. It gets its own status variable, because
`delete_one` assigns `code` in the same POSIX shell scope.

The guard refuses a constant pin, on executable lines only, so the manifest keeps
the history in prose. Two new built-in controls: one proves the detector fires
when a constant IS present, the other proves it does not read comments. Three
mutations confirmed red by hand: the identity binding deleted, a constant pin
added back, and the comparison neutered to `if false`.

`repository-retirement` joins the kernel-roll gate exemptions, for the reason
`pipelines-retirement` is already there: it runs `wait: false` and has no
readiness for the gate to read. Build 2c7f70c2 failed on that guard alone.

Claude-Session: https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8
binjovi-bot deleted branch fix/retire-hello-lib-residue 2026-09-10 19:27:18 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sean/seanfarm!595
No description provided.