Repo defaults allowed in the home file, but only inside an explicit defaults: block, merged below every repo's own layers (accepted cost: secrets may live in two places). Registry wins over cwd when a home config exists. Repo names default to the directory basename, overridable per registry entry, duplicates abort loudly. The file name stays .werkator.yml in all three locations — the location carries the meaning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
8.4 KiB
Markdown
88 lines
8.4 KiB
Markdown
# Step 22: One Werkator Instance, Many Repositories
|
||
|
||
Prerequisites: none in code; step 21's Werkdock work is independent.
|
||
Read `README.md` first.
|
||
This step is a roadmap in sessions (A–E), like step 21; each session is sized for one focused Claude Code session.
|
||
|
||
## The Problem
|
||
|
||
"One instance per repository" is a founding tenet (`docs/Werkator-Konzept.md`, AGENTS.md) — and on a Managed Webspace it does not scale even to two repositories.
|
||
Building Werkbaum next to Werkator on mih34 today means: a second pac user or a second service, a second assigned port, a second tunnel or domain, a second UI, a second metrics page.
|
||
Every repository added multiplies operations, while the instance-level resources (port, UI, watcher schedule, executor slots, metrics) could be shared.
|
||
The goal: one Werkator instance serves a *set* of repositories — one service, one port, one UI — while each repository keeps its own configuration, secrets, history, and artifacts.
|
||
|
||
## The Guiding Idea: the Repository Stays Self-Contained
|
||
|
||
Everything repository-specific already lives *inside* the repository: the machine config with secrets in `.git/werkator/`, build results, auto-build slots, worktrees, buildenvs — and the artifact store is already keyed per repo path.
|
||
The multi-repo instance therefore does not absorb repository state; it becomes an *aggregator* over self-contained repositories.
|
||
Consequences:
|
||
|
||
- Adding or removing a repository is editing a registry entry, never a data migration.
|
||
- A repository can move between instances (or back to its own) without losing anything.
|
||
- Single-repo mode stays the degenerate case: a registry of one, implicitly the current working directory — existing installations keep working without any config change.
|
||
|
||
## Key Ownership Splits
|
||
|
||
Today all config comes from the repo's own layers; multi-repo splits ownership:
|
||
|
||
- **Instance-level** (decided 2026-09-01: a `.werkator.yml` in the *home directory* of the user running the instance — the name stays `.werkator.yml` in all three locations, the location carries the meaning): `server.*` (port, bind address, public base URL / domain, nginx), the repository registry, the control token (one UI, one token), `executor.maxConcurrent` as the *global* cap, watcher interval, metrics.
|
||
- **Repo defaults** (decided 2026-09-01): the home file MAY carry defaults for repo-level keys (e.g. one `git.account`/`git.token` for all repos of the same forge), in an explicit `defaults:` block so instance keys and repo defaults never blur syntactically.
|
||
The block merges BELOW every repo's own layers: home `defaults` → committed project config → repo machine config → branch layer (pinning semantics unchanged — home and repo machine config are both host-side layers, the branch layer still cannot reach pinned keys).
|
||
Accepted cost: secrets may then live in two places; a repo without its own secrets is no longer self-contained on its own.
|
||
- **Repo-level** (unchanged, from the repo's own layers — machine config in its `.git/werkator/`, committed project config, branch layer): `gitea.*` (each repo has its own owner/repo/token/statusContext), `git.*` credentials, `builds`, retention, per-repo watcher options (e.g. `pullRequestGate`), sandbox policy and its pinning.
|
||
- **Both**: a per-repo concurrency cap below the global one may come later; not in the first cut.
|
||
|
||
The pinning model is untouched: pinned keys still come from each repo's machine config, and the branch layer still cannot reach them.
|
||
|
||
## The Sessions
|
||
|
||
### A — Decision and schema (ADR 0009)
|
||
|
||
- Write ADR 0009: revise the one-instance-per-repository tenet to one-instance-per-*set*; record the aggregator idea and the key ownership split above.
|
||
Considered alternative to record: a federation dashboard proxying several single-repo instances — less invasive, but it keeps N services/ports and solves only the UI, not the operations burden.
|
||
- Define the instance config: `~/.werkator.yml` (decided 2026-09-01) — the repository registry plus the instance-level keys above; one instance per OS user, which matches the platform model (pac users on a webspace, service users elsewhere).
|
||
`werkator server` without a home config serves the current directory exactly as today.
|
||
- Decide the transition for instance keys that today sit in a repo's machine config (mih34's carries `server.*`): once a home config exists, repo-level instance keys are ignored with a warning naming both files — never merged silently.
|
||
- Repo identity for display and routes (decided 2026-09-01): a short unique name per registry entry, defaulting to the repository's directory basename, overridable in the entry; duplicate resulting names abort the start loudly. Used as the route segment (`/repos/<name>/…`) and UI grouping key.
|
||
- Precedence (decided 2026-09-01): when a home config with a registry exists, `werkator server` serves the registry regardless of the current directory — one user, one instance, deterministic; without a home config it serves the current directory exactly as today.
|
||
- Update `docs/Werkator-Konzept.md` and AGENTS.md wording ("one instance per repository set").
|
||
|
||
### B — RepoContext refactor, behavior unchanged
|
||
|
||
- Introduce a `RepoContext` (working dir, config loading, git access, result repository, artifact store key, watcher state) and thread it through executor, watcher, and server code paths that today implicitly use the single `workingDir`.
|
||
- The executor becomes instance-global with repo-scoped pools: serialization per (repo, branch), the global `maxConcurrent` across repos; `BuildResult` needs no schema change — results stay in each repo's own JSON file, the repo dimension exists only in memory and in routes.
|
||
- Single-repo behavior, routes, and UI stay byte-identical; the full test suite is the acceptance gate.
|
||
|
||
### C — The registry and N repositories
|
||
|
||
- Load the registry, build one `RepoContext` per entry; fail the start loudly on duplicate names or unreadable repos (config-version violations abort only that repo's registration, like branch-config violations fail only that branch).
|
||
- Watcher multiplexing: one poll cycle iterates the contexts (fetch, enqueue, prune per repo) with per-repo error isolation — one unreachable origin must not starve the others; `WatcherState` gains the repo dimension for the health banner.
|
||
- Startup recovery per repo; auto-build slots stay in each repo's `.git/werkator/`.
|
||
- CLI commands gain an optional repo selector and default to the current working directory, so `werkator status` inside a repo behaves as today.
|
||
|
||
### D — Server, API, and UI scoping
|
||
|
||
- Routes gain the repo segment (`/api/repos/<name>/builds/…`, `/repos/<name>/builds/<key>`); with exactly one registered repo the today-routes keep working (redirect or alias) so bookmarks and posted Gitea links survive.
|
||
- Latest/branches/history views group by repo or gain a repo column; one instance-wide metrics page; one control token.
|
||
- Gitea status links use the repo-scoped URLs.
|
||
|
||
### E — Rollout on mih34: Werkbaum joins
|
||
|
||
- Registry with the Werkator and Werkbaum repositories under the existing user, one service, one port, the existing tunnel.
|
||
- Write Werkbaum's `.werkator.yml`: Gradle backend build and npm frontend build in the shared trimmed image (Node is already in it).
|
||
- Record the deployment; retire the second-instance/second-user idea from the notes.
|
||
|
||
## Open Questions
|
||
|
||
- Fairness across repos when the global concurrency cap is contended (round-robin per repo vs. FIFO) — decide in session C with the real queue behavior at hand.
|
||
- Whether buildenv rootfs trees should be shared across repos (today each repo unpacks its own under `.git/werkator/buildenv/`) — the natural answer is Werkdock's image store (step 21 session C), not instance-level state; until then duplicate unpacked rootfs trees are the accepted cost.
|
||
- Whether `artifactKey` needs a repo prefix or stays globally unique by construction (random suffix) — decide in session B when the routes are designed.
|
||
|
||
## Acceptance Criteria
|
||
|
||
- Session A: ADR 0009 merged; registry and key-ownership documented in `docs/configuration.md`.
|
||
- Session B: full suite green with `RepoContext` threaded through; no route or behavior change observable.
|
||
- Session C: an instance with two registered repos builds pushes in both, with per-repo error isolation proven by a test (one broken origin, the other keeps building).
|
||
- Session D: both repos browsable in one UI; single-repo installations keep their existing URLs.
|
||
- Session E: mih34 builds Werkator and Werkbaum from one service; `docs/deployment.md` describes the registry setup.
|