Declare the repos the estate wants, and what holds on them #16

Merged
larandar merged 1 commit from agent.teyla/repo-conformance into main 2026-08-10 17:42:18 +00:00 AGit
Contributor

Closes #3. Closes #5.

Both tickets ask for the same thing in different clothes: per-repo forge settings that must hold. #3 wants a push mirror lar.ad/<repo> main → midgard/<repo>; #5 wants the merge-style allow-set that keeps pre-PR commit curation (Ting/Ting#4) from being discarded by a merge-commit. One declaration in nix/forgejo.nix, beside the webhook, in Forgejo's own API field names so a reconciler can compare field-for-field.

nix build .#forgejo-repos renders it: the instance defaults, eight repos across two orgs, four public faces.

Desired state, not an inventory

Repos are declared by org, and the declaration says what the estate wants, not what the forge already has. A repository named there that does not exist is a repository to create; a mirror whose target does not exist is a target to create. Existence is a field like any other, and reconciling it is the provider's job — not a precondition this file waits for.

midgard/modron is declared for exactly that reason: #3 names modron as midgard-bound, midgard/modron does not exist, and that is the point of declaring it. mirror is a bare boolean rather than a path, so the public face always carries the same name in the public org and a typo cannot invent a second naming convention.

The converse deliberately does not hold: a repository on the forge and absent from the declaration is a finding for a person, never a deletion. Add-and-repair, the posture #3 asks for on mirrors. Nothing here should be able to destroy a repository as a side effect of an eval — which is also why midgard/fastlane (superseded per #6) is named here as an observation and not removed.

The instance defaults, upstreamed from flake-ops

modules/impls/forge/forgejo.nix sets DEFAULT_BRANCH = "fastlane", DEFAULT_MERGE_STYLE = "rebase" and DEFAULT_DELETE_BRANCH_AFTER_MERGE = true in app.ini. Declared here too — the declaration, not a second implementation. They are the same policy as the per-repo settings, split across two mechanisms by Forgejo rather than by intent, and kept in separate files they can disagree silently.

The property that makes the per-repo half necessary, measured rather than assumed:

A DEFAULT_* applies only to repositories created after it is set. All eight repos still report default_delete_branch_after_merge: false despite the instance setting it to true.

So the per-repo half re-states it, both are rendered into one document, and nix flake check compares them. default_merge_style is derived from the instance value in Nix, so they cannot drift by construction; the check guards a future edit that hardcodes it separately.

DEFAULT_BRANCH needed care rather than copying — it is per-repo truth, not a constant. Ting/Ting (law repo, PRs land on main), Ting/Jostoph (no fastlane, per this repo's #8) and lar.ad/jostoph-rs (main-only by design) each say so with a reason. The rendered result matches the live branch reality on all eight.

Both ends of a mirror

A midgard repo is where the estate publishes, not where it develops — it has shed the fastlane flow:

  • main only. fastlane there would name a branch the mirror's branch_filter exists to keep out.
  • Pull requests off. All four midgard repos currently accept them. A PR against a copy that a push mirror overwrites on a schedule targets a history that is not the target's to keep; merging one produces work the next sync silently discards.
  • A writable deploy key. use_ssh means the forge generates a key pair and exposes only the public half (PushMirror.public_key); the target must carry that half with read_only: false, because a read-only key cannot be pushed with and the mirror would fail every sync. Both ends of one credential, neither value written down here.

Public faces are derived from the same (canonical, public) pairs as the mirrors, so a mirror cannot exist without its face or the reverse — and the public org cannot be declared into by hand, which is an eval-time error.

#3's open boundary question, answered

#3 said to make the provider-vs-steward call deliberately before building. It is already ruled at the top of nix/forgejo.nix (owner, 2026-08-05), and a push mirror is the webhook case for the same mechanical reason, read off the live forge: a mirror is addressed by remote_name, which CreatePushMirrorOption has no field for — the forge invents it (remote_mirror_c7JGztIibo-). Identity you did not choose is state, and state is the provider's requireState case.

#3's "designation needs a source of truth" is answered as a declaration here rather than a repo topic: a topic makes the forge the authority on what the forge ought to be, so drift and the record of intent share one mutable home.

Five real problems this names

Measured against the live forge:

  1. lar.ad/dendrite has no push mirror at all, yet midgard/dendrite exists and is diverged — canonical 290f34b7, public 528821af. Exactly #3's predicted failure.
  2. lar.ad/jostoph-rs's mirror has an empty branch_filter — every branch, where nixops4-providers correctly has main.
  3. Every canonical repo allows merge commits and rebase-merge, default_merge_style = "merge", and none retires its branch at merge.
  4. Every midgard repo accepts pull requests.
  5. midgard/modron does not exist, though modron is meant to have a public face.

On (2) I am the evidence: I briefly created a fastlane branch on lar.ad/jostoph-rs earlier today (a mistake, corrected on #11). The empty filter published it to midgard/jostoph-rs within the hour, and deleting it from the canonical did not retract it — I removed the public copy by hand. midgard/jostoph-rs is back to main only at 029a102, so the locks in #13 and Vedanta#4 are unaffected.

Out of scope

  • Reconciling any of it. Not a narrowing of the tickets but a structural fact: this repo holds no Forgejo token, and checks.module-evaluates fails the build if the generated unit grows a curl or FORGEJO_TOKEN. A reconciler here would mean deleting that check.
  • The provider-side resource types that consume this, in lar.ad/nixops4-providers — the other half of both tickets, a separate repo.
  • Org creation. The orgs are the keys of the declaration; nixops-forgejo already reconciles orgs as a resource.

Boundary

This file declares and never reaches the network; the change keeps that — writeText and nothing else.

The declaration deliberately states policy that is currently false nearly everywhere (all five findings). That is the point: a declaration nothing has reconciled yet describes intent, it does not claim the world already matches.

The list is hand-maintained, which is the honest cost: a repo missing from it is invisible to conformance. Same gap as a repo missing a topic, but in one reviewed place rather than across N settings pages.

Acceptance

  • nix build .#forgejo-repos — instance, 8 repos in 2 orgs, 4 public faces; default_branch matches live branch reality on all eight.
  • nix flake check — green, 10 checks.
  • Mutation-tested, because a check that cannot fail proves nothing. Each applied, its named error observed, then reverted to green:
    • allow_merge_commits = truedoes not declare the fleet merge policy
    • branch_filter = "" (the real-world bug) → does not restrict itself to main
    • instance merge style ≠ rebase → the policy-literal check
    • per-repo style hardcoded away from the instance → disagrees with the instance DEFAULT_MERGE_STYLE
    • delete-branch-after-merge dropped → does not retire its branch at merge
    • public face with PRs on / read-only key / fastlane default → not main-only, PR-free and writable (three mutations)
    • a repo declared directly in the public org → eval-time midgard holds public faces, which are derived, not declared here
    • a dropped public face → the mirror/face count guard
  • The check also refuses a zero-mirror declaration and any credential-shaped string.

Two corrections made during review, both mine: an earlier revision pinned default_merge_style to the literal "rebase" and compared it to the instance, so the agreement check could not fail independently — the literal now lives in one place. And an earlier revision excluded lar.ad/modron on the grounds that midgard/modron did not exist, which contradicted this PR's own claim that a declaration states intent rather than describing the forge.

Verified with --override-input jostoph-rs path:… because this branch is off main, whose input still pins the private canonical; #13 fixes that independently.

Closes #3. Closes #5. Both tickets ask for the same thing in different clothes: **per-repo forge settings that must hold**. #3 wants a push mirror `lar.ad/<repo> main → midgard/<repo>`; #5 wants the merge-style allow-set that keeps pre-PR commit curation (Ting/Ting#4) from being discarded by a merge-commit. One declaration in `nix/forgejo.nix`, beside the webhook, in Forgejo's own API field names so a reconciler can compare field-for-field. `nix build .#forgejo-repos` renders it: the instance defaults, eight repos across two orgs, four public faces. ## Desired state, not an inventory Repos are declared **by org**, and the declaration says what the estate *wants*, not what the forge already has. A repository named there that does not exist is a repository to **create**; a mirror whose target does not exist is a target to create. Existence is a field like any other, and reconciling it is the provider's job — not a precondition this file waits for. `midgard/modron` is declared for exactly that reason: #3 names modron as midgard-bound, `midgard/modron` does not exist, and that is the point of declaring it. `mirror` is a bare boolean rather than a path, so the public face always carries the same name in the public org and a typo cannot invent a second naming convention. The converse deliberately does **not** hold: a repository on the forge and absent from the declaration is a **finding for a person, never a deletion**. Add-and-repair, the posture #3 asks for on mirrors. Nothing here should be able to destroy a repository as a side effect of an eval — which is also why `midgard/fastlane` (superseded per #6) is named here as an observation and not removed. ## The instance defaults, upstreamed from flake-ops `modules/impls/forge/forgejo.nix` sets `DEFAULT_BRANCH = "fastlane"`, `DEFAULT_MERGE_STYLE = "rebase"` and `DEFAULT_DELETE_BRANCH_AFTER_MERGE = true` in app.ini. Declared here too — the declaration, not a second implementation. They are the same policy as the per-repo settings, split across two mechanisms by Forgejo rather than by intent, and kept in separate files they can disagree silently. The property that makes the per-repo half necessary, measured rather than assumed: > **A `DEFAULT_*` applies only to repositories created after it is set.** All eight repos still report `default_delete_branch_after_merge: false` despite the instance setting it to `true`. So the per-repo half re-states it, both are rendered into one document, and `nix flake check` compares them. `default_merge_style` is *derived* from the instance value in Nix, so they cannot drift by construction; the check guards a future edit that hardcodes it separately. `DEFAULT_BRANCH` needed care rather than copying — it is per-repo truth, not a constant. `Ting/Ting` (law repo, PRs land on main), `Ting/Jostoph` (no fastlane, per this repo's #8) and `lar.ad/jostoph-rs` (main-only **by design**) each say so with a reason. The rendered result matches the live branch reality on all eight. ## Both ends of a mirror A `midgard` repo is where the estate publishes, not where it develops — it has **shed the fastlane flow**: - **`main` only.** `fastlane` there would name a branch the mirror's `branch_filter` exists to keep out. - **Pull requests off.** All four midgard repos currently accept them. A PR against a copy that a push mirror overwrites on a schedule targets a history that is not the target's to keep; merging one produces work the next sync silently discards. - **A writable deploy key.** `use_ssh` means the forge generates a key pair and exposes only the public half (`PushMirror.public_key`); the target must carry that half with `read_only: false`, because a read-only key cannot be pushed with and the mirror would fail every sync. Both ends of one credential, neither value written down here. Public faces are **derived** from the same (canonical, public) pairs as the mirrors, so a mirror cannot exist without its face or the reverse — and the public org cannot be declared into by hand, which is an eval-time error. ## #3's open boundary question, answered #3 said to make the provider-vs-steward call deliberately before building. It is already ruled at the top of `nix/forgejo.nix` (owner, 2026-08-05), and a push mirror is the webhook case for the same mechanical reason, read off the live forge: a mirror is addressed by **`remote_name`**, which `CreatePushMirrorOption` has no field for — the forge invents it (`remote_mirror_c7JGztIibo-`). Identity you did not choose is state, and state is the provider's `requireState` case. **#3's "designation needs a source of truth"** is answered as a declaration here rather than a repo topic: a topic makes the forge the authority on what the forge ought to be, so drift and the record of intent share one mutable home. ## Five real problems this names Measured against the live forge: 1. **`lar.ad/dendrite` has no push mirror at all**, yet `midgard/dendrite` exists and is **diverged** — canonical `290f34b7`, public `528821af`. Exactly #3's predicted failure. 2. **`lar.ad/jostoph-rs`'s mirror has an empty `branch_filter`** — every branch, where `nixops4-providers` correctly has `main`. 3. **Every canonical repo allows merge commits and rebase-merge**, `default_merge_style = "merge"`, and **none retires its branch at merge**. 4. **Every midgard repo accepts pull requests.** 5. **`midgard/modron` does not exist**, though modron is meant to have a public face. On (2) I am the evidence: I briefly created a `fastlane` branch on `lar.ad/jostoph-rs` earlier today (a mistake, corrected on #11). The empty filter published it to `midgard/jostoph-rs` within the hour, and **deleting it from the canonical did not retract it** — I removed the public copy by hand. `midgard/jostoph-rs` is back to `main` only at `029a102`, so the locks in #13 and Vedanta#4 are unaffected. ## Out of scope - **Reconciling any of it.** Not a narrowing of the tickets but a structural fact: this repo holds no Forgejo token, and `checks.module-evaluates` **fails the build** if the generated unit grows a `curl` or `FORGEJO_TOKEN`. A reconciler here would mean deleting that check. - **The provider-side resource types** that consume this, in `lar.ad/nixops4-providers` — the other half of both tickets, a separate repo. - **Org creation.** The orgs are the keys of the declaration; `nixops-forgejo` already reconciles orgs as a resource. ## Boundary This file declares and never reaches the network; the change keeps that — `writeText` and nothing else. The declaration deliberately states policy that is currently **false nearly everywhere** (all five findings). That is the point: a declaration nothing has reconciled yet describes intent, it does not claim the world already matches. The list is hand-maintained, which is the honest cost: a repo missing from it is invisible to conformance. Same gap as a repo missing a topic, but in one reviewed place rather than across N settings pages. ## Acceptance - `nix build .#forgejo-repos` — instance, 8 repos in 2 orgs, 4 public faces; `default_branch` matches live branch reality on all eight. ✅ - `nix flake check` — green, 10 checks. ✅ - **Mutation-tested**, because a check that cannot fail proves nothing. Each applied, its named error observed, then reverted to green: - `allow_merge_commits = true` → `does not declare the fleet merge policy` ✅ - `branch_filter = ""` (the real-world bug) → `does not restrict itself to main` ✅ - instance merge style ≠ `rebase` → the policy-literal check ✅ - per-repo style hardcoded away from the instance → `disagrees with the instance DEFAULT_MERGE_STYLE` ✅ - delete-branch-after-merge dropped → `does not retire its branch at merge` ✅ - public face with PRs on / read-only key / `fastlane` default → `not main-only, PR-free and writable` (three mutations) ✅ - a repo declared directly in the public org → eval-time `midgard holds public faces, which are derived, not declared here` ✅ - a dropped public face → the mirror/face count guard ✅ - The check also refuses a zero-mirror declaration and any credential-shaped string. ✅ Two corrections made during review, both mine: an earlier revision pinned `default_merge_style` to the literal `"rebase"` *and* compared it to the instance, so the agreement check could not fail independently — the literal now lives in one place. And an earlier revision excluded `lar.ad/modron` on the grounds that `midgard/modron` did not exist, which contradicted this PR's own claim that a declaration states intent rather than describing the forge. Verified with `--override-input jostoph-rs path:…` because this branch is off `main`, whose input still pins the private canonical; #13 fixes that independently.
agent.teyla force-pushed agent.teyla/repo-conformance from aae801539b to f1d4ccce17 2026-08-10 09:00:52 +00:00 Compare
agent.teyla changed title from Declare the repo settings that must hold to Declare the repo settings that must hold, both ends 2026-08-10 09:01:29 +00:00
agent.teyla force-pushed agent.teyla/repo-conformance from f1d4ccce17 to bbf39e7b5c 2026-08-10 10:02:38 +00:00 Compare
agent.teyla changed title from Declare the repo settings that must hold, both ends to Declare the repos the estate wants, and what holds on them 2026-08-10 10:03:15 +00:00
Sign in to join this conversation.
No reviewers
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
Ting/Jostoph!16
No description provided.