Files
hq/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
T
jochen 4a1b218706 Seats held by assignments, one assignment per module per node, and 0113's bottom of the stack
Decided with the author:
- A seat is held by one assignment, not claimed by a definition. A definition says which seats a module
  can hold; an assignment says which it does. The store module can run on every node and one assignment
  holds mesh-store; moving a role changes an assignment, never a definition. The foundation's seats name
  what the mesh itself uses and route no consumer — database and amqp consumers use co-location, the
  holder included. This replaces the wrong rationale that the foundation's store is "provider to nobody",
  which contradicted ADR 0078 and to-be 21. 0079's one-postgres rule becomes one mesh-store holder.
- A module is assigned at most once to a node. The instance identity in 0112 and 27 is withdrawn, and the
  login-length problem with it.

Review fixes to 0113:
- The bottom of the stack: the vault is installed as soon as the shared runtime base exists, and genesis
  generates everything needed until then — including the permanent controller's, the control-node
  agent's, the builder's and the broker provisioner's bus accounts, and the controller's store login.
  Genesis creates those accounts until the broker's provisioner runs and adopts them.
- Genesis's values are delivered recorded as the mesh's own, so 0092's never-replace rule for operator
  values does not make them unrotatable.
- Backend-issued secrets (a forge's once-only API token) enter through the vault. Non-module parties
  (the controller's logins, node agents' accounts) are answered the same way, the controller asking on
  their behalf; an enrolment token reaches the controller only as what verifies it.
- A secret with no provisioner to apply it is marked not rotatable by the mesh and refused, instead of
  a restart reported as done. Unused password generators in six provider clients are removed, and a
  catalogue scan checks no module mints.
- Rotation's lock-out cases (offline reader, bus account owner, restarted provisioner) are recorded as
  open, with overlap and re-confirm-with-safeguards as the two answers, to be chosen before acceptance.

0110, 0111 and 26 are marked proposed: they changed in meaning and are under review, and an accepted
record must not rest on proposed ones. To-be 23 and the glossary are restored to main; they change when
these records are accepted.
2026-09-25 23:47:26 +02:00

5.5 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
building it proposed 2026-09-25 jochen false 0069-a-module-is-a-repository-and-a-path.md

111. A build source is on the mesh's git seat, or it is an external repository

Context

ADR 0069 made a module a repository, a path and a ref, and the controller records all three against the module so it can rebuild it and say when its source has moved ahead. The repository is recorded exactly as a person typed it. build <repository> hands the string to a build machine, which runs git clone on it, and the same string becomes the module's recorded source.

So a self-hosted forge's address is written into every module built from it. The mesh runs its own forge, and most of what it builds lives there. Every one of those modules carries the forge's scheme, host and port in its recorded source. Move the forge to another machine, or change the port it is published on, and every recorded source is stale at once. Nothing notices until a rebuild fails to clone.

And nothing names the mesh's git at all. gitea serves git over HTTP and over SSH, and the mesh's vocabulary contains neither. No provision, no serves, no seat, as the forge survey (research 013) found. The only trace is a label on its public route, which the mesh is explicitly not meant to interpret.

External repositories are ordinary, and must stay so. An application the mesh hosts may live on a public forge. Building it from its URL works today and must keep working unchanged.

Considered Options

1. Keep recording literal URLs. Rejected. It is the problem: the forge's address copied into every module built from it.

2. Recognise a self-hosted source by matching its URL against the forge's current address. Rejected. It infers the kind of source from the shape of a string, and the inference fails in the one case it exists for: after the forge moves, old URLs no longer match anything.

3. Two explicit forms: a repository on the holder of the git seat, or an external URL. Chosen.

Decision

The mesh has a git seat. It is mesh-scoped and delivers the git provision, per ADR 0110. Its holder provides git, serving how a repository on it is cloned: the scheme and the port. A gitea assignment holds it.

A source is on the git seat, or it is external, and the mesh records which.

  • build --self <owner>/<repository> builds from a repository on the seat's holder. The recorded source is the repository's path on that holder, and the seat it is on. It never contains an address. At the moment of building, the controller composes the clone URL from where the holder runs and what it serves for git, so a moved forge changes nothing recorded.
  • build <url> is unchanged: an external repository, recorded and cloned exactly as given. GitHub and GitLab are the ordinary cases.

An unheld seat refuses self-hosted builds and nothing else. With nobody holding git, build --self is refused, naming the seat and saying what would hold it. External builds are unaffected. A mesh without a forge of its own builds from external repositories only, and says so rather than failing to clone.

The build machine is not told the difference. It receives a URL either way. Composing the URL is the controller's job, because only the controller knows where the seat's holder runs.

Consequences

  • The controller's inventory gains a column saying which seat a source is on. It is empty for every module recorded before this, which is correct: they were all recorded as literal URLs.
  • build, build --behind and build --dry-run resolve a seat source before asking a builder. The recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because that is what happened.
  • gitea can hold git and provides it, serving HTTP clone on its web port; the forge's assignment holds the seat.
  • Not decided: credentials for private repositories. The mesh's own repositories are public, and clone without one. A private repository still works only if the build machine's own git configuration authenticates, exactly as before. Delivering a clone credential through the git provision's grant is the obvious next step, and it is its own decision.
  • Not changed: modules already recorded from the forge keep their literal URLs until they are rebuilt with --self. Rewriting them in place would be the URL-matching this record rejects.

How it is checked

Rule Checked by
A seat source records no address A controller test resolves a seat source and asserts the recorded repository is the path alone.
The URL comes from the holder A test composes the clone URL from a holder's node address and served git scheme and port, and a second where the port was moved on the node.
An unheld seat refuses self-hosted builds only A test with no holder: --self is refused naming the seat; an external URL passes through unchanged.

References

  • ADR 0069: a module is a repository, a path and a ref
  • ADR 0110: seats, and a seat delivering a provision
  • Research 013: git is served and declared nowhere
  • mesh-controller cmd/mesh-controller/build.go, internal/builder/builder.go