`init --systemd` generated the systemd unit, the .htaccess and the maintenance
page from a configuration it never actually read reliably: the load swallowed
every exception and fell back to a default `ServerConfig`, and it resolved the
layers from the process's current directory instead of the git top level every
other file of the command goes through.
A broken `.werkator.yml` was therefore indistinguishable from an unconfigured
`publicBaseUrl` — the host integration was skipped without a word — and running
`init` from a subdirectory read a foreign configuration or none, which also
defeated `--apply`'s promise that the fragment reaches the generated unit.
The fallback stays, but the exception message is printed, once per run, and the
repository root is passed down like every other caller of `ConfigLoader.load`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six explored directions, the chosen one (E) worked out in a light and a dark
teal palette, with a repository strip previewing every served repository,
the ten-repository and the phone case, and the backend it needs (/api/repos).
Renderings of all artboards live next to the RFC; AGENTS.md registers docs/rfcs/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The build of branch-a slept one second while the test asserted, without
any synchronization, that branch-b was still PENDING.
Under CPU contention on the mih09 host the sleep could elapse first, so
branch-b was already RUNNING or SUCCESS when the assertion ran.
branch-a now blocks until the test creates a gate file, and the test
first waits for branch-a to be RUNNING; the PENDING assertion no longer
depends on timing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A deployment's ErrorDocument-covered downtime window looked dead: Apache's
default 502/503/504 error page, or a hung request, while instance-update
briefly restarts the systemd unit. init --systemd now also writes a static
werkator-maintenance.html next to the generated .htaccess, and the .htaccess
maps a refused connection straight to it — served by Apache alone, no
change needed to Werkator itself since it is exactly the process that is
down.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The system page showed the disk of the host volume, not the budget the
instance can actually fill — on a Hostsharing Managed Webspace that is a
group quota, tighter than the volume by an order of magnitude, so the
warn/critical highlighting could never fire before a build failed with
"Disk quota exceeded".
DiskQuota parses `quota -u -g --no-wrap --raw-grace` and picks the
tightest of the user quota, the group quota, and the volume itself;
SystemMetricsCollector reports whichever binds, resets the disk
min/max/avg when the binding source changes, and the system page names
the source in its info line. A host without a binding quota (Docker
hosts, developer machines) renders exactly as before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die alte GitHub-PR#14 kollidiert mit der Gitea-Nummerierung, die nach dem
Umzug bei 1 neu beginnt; PR#16 ist die tatsächliche Nummer auf git.javagil.de.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Volume-Zahlen sind nicht mehr nur Rückfall, sondern dritter Kandidat: Gebunden
ist, was am wenigsten Platz lässt; eine später eingeführte User-Quota geht ohne
Änderung in den Vergleich ein.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Auf mih09 zeigt /system die 71-GiB-Platte des Hosts, gemessen bindet aber die
Gruppen-Quota des Pakets (8 GiB soft, 12 GiB hard, 1,04 GiB belegt). Das PR-Doc
plant: quota(1) per CLI lesen, die bindende Zeile (User/Gruppe, Dateisystem des
Repos) als Disk-Budget nehmen, ohne Quota unverändert der File Store, Serien-Reset
bei Quellenwechsel, Budget in der Info-Zeile benannt. Nur Plan, kein Code.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Werkator's canonical repository now lives on git.javagil.de; GitHub stays
around as a public mirror. This keeps its main branch in sync without
touching any other branch or the repo's other history.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A version bump is manual and easy to forget on a deployment — it happened
twice on 2026-09-03, leaving a running instance whose footer/--version
claimed a version stale by two deployments. ReleaseVersionConsistencyTest
now compares build.gradle.kts against the top entry of releases.html so a
forgotten bump, or notes written for a version nothing was built as, fails
before an artifact can even be produced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Link-Leiste neben dem Menü entfällt. An ihrer Stelle steht dort, wo
bisher der Repository-Name stand, eine Auswahlliste der bedienten
Repositories; werkator.js hängt die aktuelle Ansicht (branches, history,
current) an, damit der Wechsel auf derselben Seite bleibt. Mit nur einem
Repository bleibt der Name wie bisher reiner Text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Die offene Frage im PR-Dokument sagte „löst sich beim Merge". Das Log sagt
es genauer: Am Merge-Commit wurde `werkdock` noch eingereiht, erst am
nächsten nicht mehr. Die Projektschicht ist die Datei im Arbeitsbaum des
beobachteten Klons — die Definition überlebt, bis dieser Baum an der
Entfernung vorbei ist, der Merge selbst bekommt also einen letzten
Geister-Build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Geschrieben wurde es als #14 — vergeben wurde beim Öffnen #15. Die
Namenskonvention (docs/prs/README.md) will die Nummer aus dem Tracker,
sonst zeigt später nichts mehr aufeinander.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: werkdock/ verlässt dieses Repository
Die neun Commits unterhalb von werkdock/ sind per `git subtree split`
historieerhaltend nach https://git.javagil.de/mi/werkdock gehoben; hier
bleibt nichts davon zurück.
Mit dem Verzeichnis geht die Build-Definition `werkdock`: Sie beschrieb ein
Go-Modul, das der Kotlin-Build um sie herum nicht berühren kann — jeder
Werkator-Branch baute es dennoch als `<branch>@werkdock` mit. Drüben ist
sie die einzige und heißt `default`.
Die Kopplung bleibt, wie Sitzung C sie gemacht hat: Werkator ruft das
`werkdock`-Binary über PATH (`bwrap.werkdock`, weiterhin gepinnt), so wie
es `git` ruft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(remote): das werkdock-Binary kommt aus einem eigenen Checkout
Bisher baute das Skript es aus dem Unterverzeichnis. Jetzt nennt
WERKDOCK_REPO den Checkout (Vorgabe: ein Geschwister dieses Repositories —
die übliche Ablage, wenn man an beidem arbeitet), WERKDOCK_BINARY das
gebaute Binary darin; beides ist wie jeder Transportwert aus der Umgebung
übersteuerbar.
Fehlt der Checkout, nennt die Meldung die Klon-URL, statt bloß „file not
found" zu sagen: Das Skript kann die Lage nicht mehr selbst beheben, also
sagt es, was sie beheben würde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: Sitzung E — Werkdock hat ein eigenes Repository
Plan-Schritt 21 bekommt die Sitzung, die die Herauslösung beschreibt, samt
Abnahmekriterium; Plan-Index und Architektur-Skill sagen nicht mehr, sie
stehe noch bevor, und der Deployment-Leitfaden verlinkt das Repository
statt eines Verzeichnisses, das es hier nicht mehr gibt.
Dazu das PR-Dokument nach Hausregel (docs/prs/README.md).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(remote): repo-add wendet das Instanz-Fragment an
`repo-add` rief `werkator init` ohne `--apply`, `repo-init` dagegen mit.
Folge: Ein so aufgenommenes Repository bekam die Sandbox-Politik nicht —
`bwrap.rootfs` und `bwrap.werkdock` fehlten, und der erste Build lief auf
dem nackten Host statt in der Sandbox. Gefunden beim Aufnehmen von
Werkdock: `gofmt: command not found`, weil die Go-Toolchain nur im
Sandbox-Image liegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(remote): repo-add erkennt einen ~/-Registry-Eintrag, und der Kommentar sagt die Wahrheit
Die Registry-Prüfung suchte nur den absoluten Pfad und meldete deshalb
„not registered yet“, obwohl der Eintrag als `path: ~/werkdock` dasteht —
eine Einladung, ihn ein zweites Mal einzutragen. Gegriffen wird jetzt
lokal, damit beide Schreibweisen ohne zweite Schicht Remote-Quoting
treffbar sind.
Dazu der Kommentar über dem Laden der Env-Datei: Er behauptete, die
ausdrückliche Umgebung gewinne. `set -a; source` weist unbedingt zu — die
Datei gewinnt. Beim Nachziehen des Werkdock-Fragments hat mich genau das
eine Runde gekostet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
/repos/<name>/… und /api/repos/<name>/… neben den bisherigen Pfaden; die
Kopfzeile bekommt einen Repo-Umschalter, sobald mehr als ein Repository
registriert ist, und die Gitea-Statusmeldung trägt eine dauerhafte URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Instanz liest ihre Repositories aus ~/.werkator.yml (ADR 0009); Watcher,
Build-Ausführung und Artefakte arbeiten je Repository. Bisher lief sie auf
einem Vorab-Build dieses Branches — mit dem Merge entspricht main wieder dem,
was produktiv läuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tools/remote` konnte genau ein beobachtetes Repository einrichten
(`repo-init`, fest auf $WERKATOR_PATH/werkator). Für eine Instanz, die eine
Registry bedient (ADR 0009), fehlte der Weg, ein zweites hinzuzunehmen —
und das braucht der Rollout unabhängig davon, welche Repositories es am Ende
sind.
`repo-add <url> [name]` klont daneben, führt `init` darin aus und PRÜFT, ob
die Registry den Pfad schon nennt — geschrieben wird sie nicht. `~/.werkator.yml`
ist die Datei der Instanz: Port, globale Nebenläufigkeit, womöglich geteilte
Zugangsdaten. Ein Skript, das sie in place umschreibt, verändert die
Konfiguration des Betreibers hinter dessen Rücken. Klonen und Initialisieren
ist mechanisch, Registrieren ist eine Entscheidung — also druckt es den
Eintrag und sagt, dass die Registry beim Start gelesen wird.
Der Name muss ein Pfadsegment sein: Er wird das Routen-Segment /repos/<name>.
docs/deployment.md verweist im Registry-Abschnitt darauf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der letzte offene Punkt der Sitzung D („Gitea status links use the repo-scoped
URLs"). Beim Nachsehen war es kein Scoping-Problem, sondern ein fehlendes
Feature: `server.publicBaseUrl` ist als „used for all links posted to Gitea"
dokumentiert, der Executor postete aber `targetUrl = null` — es gab überhaupt
keinen Link. Jetzt zeigt der Status auf die Artefaktseite des Builds, also auf
die Logs, um die es geht. Ohne `publicBaseUrl` bleibt er null: ein relativer
Link in einem Gitea-Status wäre schlimmer als keiner, er löste gegen die Forge
auf.
Die Präfix-Regel wohnt jetzt an einer Stelle (`RepoLinks`), weil drei
dasselbe damit tun: die Seiten, die API-Antworten und diese Ziel-URLs — und
eine dreimal ausgeschriebene Regel driftet. Die Slice-Tests binden die echte
Komponente per @Import ein, statt die Regel im Test nachzubauen.
Ein neuer Test (mit publicBaseUrl trägt der Status die Artefaktseite; die
vorhandenen Tests halten fest, dass ohne sie weiterhin null gepostet wird).
499 Tests grün, ktlint sauber. Plan und Architektur-Skill nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der aus Sitzung C nach E übertragene Doku-Punkt: docs/deployment.md
beschreibt die Registry jetzt betrieblich — klonen, vorbereiten, in
~/.werkator.yml eintragen, neu starten. Dazu die Regeln, die beim Eintragen
weh tun, wenn man sie nicht kennt: Der Name ist Routen-Segment und
Umschalter-Eintrag und muss eindeutig sein (ein Doppel bricht den Start ab,
ebenso ein Eintrag, der kein git-Repository ist), eine unlesbare Konfiguration
überspringt nur ihr Repository, geteilte Zugangsdaten können einmal im
defaults-Block liegen, und die Unit behält ihren Namen — sie gehört der
Instanz, nicht einem Repository.
Dazu, was die Adressen tun: mit einem Repository bleibt jede URL, wie sie
war; mit mehreren tragen Seiten und API das Repository, und die unscoped
Pfade meinen weiterhin das bediente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zweite Hälfte von Sitzung D (docs/plan/22-multi-repo.md, PR #13): Der Server
bediente bisher genau ein Repository der Registry. Jede Route — API, Seiten,
Artefakt-Dateien — arbeitete auf `registry.current()`; ein zweites
registriertes Repository wurde gebaut und gepollt, war aber unsichtbar und
unerreichbar.
Jeder Controller löst sein Repository jetzt je Anfrage auf, statt das
bediente als Bohne zu halten; jede Route ist zweimal gemappt. Die unscoped
Form ist kein Übergangs-Alias, sondern dauerhaft die Art zu sagen „das
bediente Repository" — Lesezeichen und die nach Gitea geposteten Links
kennen kein Segment.
Entschieden gegen die Repo-Spalte: Die Seiten bleiben je Repository, die
Navigation bekommt einen Umschalter. Die Aktionen einer Zeile brauchen das
Repository ohnehin, Branches kommen von einem origin und Artefakte aus einem
Store — und bei dem einen Repository, das die meisten Installationen haben,
wäre eine Spalte nur Rauschen.
Das Link-Präfix folgt der ZAHL der bedienten Repositories, nicht dem Weg, über
den eine Seite erreicht wurde: mit einem behält die Installation ihre
bisherigen URLs (Abnahmekriterium der Sitzung), mit mehreren benennt jeder
Link sein Repository. werkator.js liest das Präfix einmal aus einem
`werkator-repo-base`-Meta. `BranchPermalinks.permanentUrl` bekommt es
ebenfalls — der permanente Schlüssel ist ein Hash des Build-Namens allein,
zwei Repositories mit `main` teilten sich sonst eine permanente URL.
Fünf neue Tests, Gegenprobe per Mutation gezogen (Präfix fest auf leer →
genau der Mehr-Repo-Test fällt). 498 Tests grün, ktlint sauber. PR-Dokument
docs/prs/2026-09-03-PR#13-…, Plan, Architektur-Skill und AGENTS.md
nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sitzung D, erste Hälfte (docs/plan/22-multi-repo.md): Jede Route der
Builds-API gibt es jetzt zweimal — repo-benannt und unscoped. Die unscoped
Form bleibt dauerhaft und meint das bediente Repository
(`RepoRegistry.current`): Lesezeichen, die alte Oberfläche und die bereits
nach Gitea geposteten Links kennen kein Repo-Segment, und eine CI, die ihre
eigenen alten Links bricht, ist eine CI, der niemand traut.
Der Controller hält kein `BuildResultRepository`, keinen `ArtifactStore` und
keinen `RepoContext` mehr als Bohne, sondern löst je Anfrage über die
Registry auf. Ein unbekannter Name beantwortet sich in der Form dieser API
(404 mit `error`) über einen eigenen ExceptionHandler statt über Springs
Problem-Detail.
Dabei gefunden und mitbehoben: `cancel` griff über Repository-Grenzen. Der
Executor bricht per Schlüssel instanzweit ab — eine Route, die ein
Repository benennt, darf damit kein fremdes erreichen. Sie prüft jetzt
zuerst, ob der Schlüssel in den Ergebnissen DIESES Repositories steht; ein
wartender oder laufender Build hat dort immer sein PENDING/RUNNING.
Drei neue Tests: die repo-benannte Route antwortet, ein unbekannter Name
gibt 404 mit Meldung, und `cancel` erreicht keinen fremden Build. 496 Tests
grün, ktlint sauber.
Noch nicht scoped: die Oberfläche und die Artefakt-Routen — die kommen mit
der zweiten Hälfte von Sitzung D.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der letzte Übertrag aus Sitzung C: `RunningBuild` trug kein Repository, der
Executor ist aber instanzweit. Zwei Folgen, beide gemessen und jetzt behoben:
- Das Worktree-Aufräumen schützte die Worktrees ALLER Repositories. Ein
laufender Build eines anderen Repositories auf einem gleichnamigen Branch
hielt hier einen Worktree fest, dessen Branch längst von origin weg war.
- Die Current-Builds-Ansicht und `/api/builds/current` zeigten die Builds
aller Repositories, schlugen ihren Status aber in den Ergebnissen NUR
dieses Repositories nach — ein fremder Build fiel auf RUNNING zurück und
zeigte einen Zustand, den niemand aufgezeichnet hat. Dasselbe beim
Live-Log: der Schlüssel eines fremden Builds wurde beantwortet.
`RunningBuild.repo` ist der `RepoContext` selbst, verglichen wird per
Referenz — der Kontext ist die Identität (ADR 0009), und der Executor hat ihn
in `startBuild` ohnehin zur Hand. Watcher, API und UI filtern damit auf ihr
eigenes Repository.
Drei neue Tests, Gegenprobe per Mutation gezogen: Nimmt man die drei Filter
wieder heraus, fallen genau diese drei und sonst keiner. 493 Tests grün,
ktlint sauber.
Architektur-Skill und Plan (docs/plan/22-multi-repo.md) nachgezogen — die
Aussage „RunningBuild carries no repository" stimmte nicht mehr. Was von
Sitzung D bleibt: die repo-bezogenen Routen und die Oberfläche, die weiterhin
nur `registry.current()` bedienen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Datei lag als `PR#000`-Platzhalter, wie es die Konvention in
docs/prs/README.md vorsieht: Die Nummer gibt es erst, wenn der Pull Request
offen ist. Er ist es — #12. Umbenannt samt der fünf Szenario-IDs
(`Scenario#000.01` … `.05` -> `Scenario#12.01` … `.05`), wie es Schritt 21
für PR #4 vorgemacht hat; sonst zeigen die IDs auf eine Nummer, die es nicht
gibt. Kein `#000` bleibt in der Datei, keine andere Datei verweist auf den
alten Namen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The metrics page sums the registered repositories' sizes; the instance
configuration is documented in docs/configuration.md, the architecture skill,
AGENTS.md and the step 22 plan (session C ticked, fairness decided FIFO).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The RepoOption mixin resolves the name through the registry; without it a
command means the current directory when served, else the first entry. An
unknown name is a usage error naming the registered repositories.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
start takes the served repositories; one poll cycle polls each in its own guard
and aggregates the reports into WatcherState, which gains a per-repository list.
With one repository the top-level fields read exactly as before; with several,
every message is prefixed by the repository name. Log lines carry the name too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RepoRegistry opens one RepoContext per entry of the home configuration, or the
current directory without one. An entry that is no git repository or a name
used twice aborts the start naming the home file; a repository whose config
must not be read is skipped with an error and the others are served. The
current-repo bean now comes from the registry (the cwd when served, else the
first entry), and the pre-rename state-dir migration runs per opened repository.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ConfigLoader reads the home file (WERKATOR_HOME overrides the location): its
defaults block merges below every repository's layers, and once it exists the
server section, executor and watcher.pollInterval come from it alone — a
repository file still carrying them is told so once, naming both files.
InstanceConfig binds the registry entries; nothing reads them yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: RepoContext over the current repository
A RepoContext bundles a repository's primary checkout with the state that lives
inside or is keyed by it (results, artifact store) and carries its name. Today
there is exactly one, opened over the current working directory; the result and
artifact-store beans now come from it, so nothing else changes yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the watcher polls a RepoContext
start/poll/recoverOnStartup take the context instead of a working directory and
read results and artifacts from it; the per-repository poll memory (logged fetch
error, deprecation warning, cached branch definitions) moves into a RepoWatch
keyed by context, so the next session can iterate contexts without one
repository's outage silencing another's. The shared WatcherState is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the executor runs builds of a RepoContext
startBuild takes the context first; builds serialize per (context, branch) and
share the global maxConcurrent cap across repositories, results and artifacts go
to the build's own context. ConsoleBuildRunner, the build/retry commands and the
builds API restart pass the current repository's context along.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the UI and the branch listing read their RepoContext
UiController and BuildsApiController take the current repository's context
instead of a settable working directory; BranchListing lists the branches of a
context and reads the results from it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: document the RepoContext, PR-doc for PR #11
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
ADR 0009 and step 22 plan: one Werkator instance serves a set of repositories
# Conflicts:
# docs/plan/22-multi-repo.md
# docs/prs/2026-09-01-PR#6-werkdock-bootstrap.md
The wrapper's config writing is gone: --env-file FILE selects the
target instance (default .env; named like docker's flag — --env means a
single variable there), the init fragment named by WERKATOR_INIT_CONFIG
is uploaded and installed remotely via 'werkator init --apply', and
instance-start places the .htaccess that init now generates. The
heredoc/sed machine-config writing is deleted; control-token delegates
to the werkator CLI; check-prerequisites uploads werkdock and runs its
doctor — tools/werkator-build-prerequisites.sh retires. The idle-check
and port-forward port lookups read the effective config via
config:print, so a port living in the applied fragment is found too.
Verified live against mih34 with the .env.mih34 + .env.mih34.yml pair
(gitignored via the new /.env.* rule): update, doctor PASS 6/6,
repo-init applying the fragment as the new layer, instance-start
placing the generated proxy and restarting the unit, control-token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'werkator init --apply FILE' installs a config-schema YAML fragment as
its own layer: validated strictly (an unknown key is refused loudly,
never ignored — a typo must not install a silent no-op), then copied
verbatim to .git/werkator/.werkator.applied.yml, above the project
config and below the hand-edited machine config, which always wins.
Deviation from the plan sketch, recorded there: a separate layer
instead of an in-place merge, because merging would re-serialize the
machine config — destroying its comments and rewriting the file that
holds the secrets; re-apply is a plain file replacement.
init --systemd now also generates werkator.htaccess beside the units
whenever a publicBaseUrl is configured — generated host integration for
the managed-webspace Apache, copied into the docroot by the wrapper.
New subcommand 'werkator control-token' prints (and lazily creates) the
token via ControlTokenService, so no wrapper needs its own generator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refinement decided 2026-09-01: each side gets its native format — the
wrapper keeps a bash-sourceable transport env file, Werkator takes a
YAML fragment in its own config schema via 'init --apply FILE',
deep-merged idempotently. The env-to-config mapping table disappears
entirely: the fragment IS configuration in the one schema, validated by
the existing binding, documented by the existing reference. The env
file names the fragment (WERKATOR_INIT_CONFIG), keeping one entry point
per instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decided 2026-09-01: Werkator becomes the executing app, the remote
script a thin wrapper (build, transport, remote execution, service
switching). Parameters travel as env files instead of many options —
'remote --env .env.mih34 werkator ...' selects the target, and
'werkator --env ... init' reads the same file and writes real values
into the files it owns, idempotently; the wrapper's YAML heredocs and
sed patches (the duplicate-block class) die. control-token becomes a
werkator subcommand, check-prerequisites delegates to werkdock doctor.
Explicitly not the removed legacy env-to-YAML conversion: the env file
is setup-time input to init, never runtime configuration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/remote reworked to role-named commands (the builder-vs-built
tangle noted 2026-09-01): instance-install/instance-update/
instance-start manage the installed Werkator + werkdock, repo-init the
watched repository. The self-build is retired — no repo clone for
building, no GitHub-key step; the instance installs from locally built
artifacts (ADR 0006), the watched repo clones anonymously via https.
instance-update refuses to swap the runtime under a running build
(FORCE=1 overrides) and keeps the previous runtime as the rollback
asset. repo-init is idempotent: checksum-skipped rootfs upload, and the
machine-config guard is fixed — its indentation mismatch (grep for two
spaces, block written with four) appended the bwrap block on every run,
the source of the nine duplicates found on mih34. The retired
install/build/start commands fail loudly naming their successors.
docs/deployment.md gains the Hostsharing Managed Webspace as the third
deployment variant, written from the live-verified setup: both new
commands ran against mih34 (full update cycle; fully skipping re-init).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runner no longer assembles raw bwrap argv: it checks image
existence via 'werkdock images', loads the rootfs archive once per
source as image werkator-buildenv-<hash> into werkdock's store (shared
across every repository of the OS user — resolving step 22's
buildenv-sharing question), and runs builds through 'werkdock run --rm'
with the git-metadata mask expressed as ordered -v/--tmpfs flags.
New pinned config key bwrap.werkdock (the executing binary, default via
PATH) — a branch must not substitute the executing binary; synced in
WerkatorConfig, BwrapOverrides, the pinned-key strip, the init
template, docs/configuration.md, and AGENTS.md.
werkdock's --clearenv means the server environment no longer leaks into
builds; the TMPDIR workaround is gone with the raw invocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The image-name pattern allows dots, so a directory like broken.tmp —
an interrupted load's staging dir — counted as an image. List now skips
.tmp names and Load refuses them, closing the ambiguity the previous
commit's (accidentally pushed red) test exposed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session C groundwork: Werkator's git-metadata mask needs a tmpfs
BETWEEN binds (ro-bind .git, tmpfs .git/werkator, bind workspace), so
-v and --tmpfs now collect into one ordered mount list and RunSpec
carries Mounts instead of Binds. --tmpfs DEST is the docker flag of the
same name. `werkdock images` lists loaded image names one per line, so
a consumer can check existence through the CLI. -v accepts the explicit
:rw docker default instead of refusing it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the tenet revision and its shape: instance registry and
instance keys in ~/.werkator.yml (one instance per OS user), optional
repo defaults in an explicit defaults: block merged below every repo's
own layers, repo config unchanged in each repository, registry-wins
precedence, basename repo names. Rejected: a federation dashboard over
single-repo instances (keeps N services), and the status quo.
Konzept/AGENTS architecture wording changes only when the
implementation lands; the decision list carries 0009 now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Host/instance configuration (domain, port, registry, global concurrency)
lives in a .werkator.yml in the home directory of the user running the
instance — one instance per OS user, matching the platform model; repo
configuration stays in each repository's .git/werkator/. Repo-level
instance keys get ignored with a warning once a home config exists.
The open questions now name the decisions still needed: repo defaults
in the home file, cwd-vs-registry precedence, repo naming, and whether
the third .werkator.yml location should carry a distinct name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-instance-per-repository tenet does not scale to a second
repository on a webspace (second service, port, tunnel, UI). Step 22
plans the revision in five sessions: ADR 0009 + instance registry and
key-ownership split, a RepoContext refactor with unchanged behavior,
watcher/executor multiplexing with per-repo error isolation, repo-scoped
routes and UI with single-repo back-compat, and the mih34 rollout with
Werkbaum as the second repository. Guiding idea: the repository stays
self-contained (state in its own .git/werkator), the instance is only
an aggregator — adding a repo is a registry entry, never a migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openjdk-21-jdk-headless instead of the full JDK (drops the X11 and
fontconfig library stack; headless AWT still works for tests),
message catalogs except en/de removed, man pages, package docs, apt
lists and cache cleared after install.
Measured on the JDK+Go+Node image: 516 MB -> 351 MB compressed —
smaller than the original JDK-only archive (375 MB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/remote mixes commands that manage the builder (the installed
Werkator instance) with commands that act on the built (the watched
repository); on the self-building instance both roles coincide in one
product, which misleads. Session D's replacement must name the role in
every command and delegate built-side operations to the werkator CLI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every artifactDir landed below reports/ — the legacy
archived_artefact_dir_path layout — which mislabeled non-report outputs
(reports/werkdock/dist/werkdock) AND hid them: the artifact page's
report index only scans reports/ for HTML pages, so a built binary was
stored but never shown.
Now build/reports keeps archiving as reports/ (the browsable anchor and
every existing link), every other directory archives at its
workspace-relative path, and the artifact page gains a plain-files list
for everything outside reports/ (log files stay in their own section;
capped at 200 entries). Existing stored artifacts keep their old layout
and remain served; only new builds use the new one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bwrap inherits the server's environment; on hosts with pam_tmpdir that
includes TMPDIR=/tmp/user/<uid>, which does not exist on the sandbox's
fresh tmpfs /tmp. Every tool honoring TMPDIR then fails — seen live on
mih34 as go's 'creating work dir: stat /tmp/user/120957: no such file
or directory'. The JVM ignores TMPDIR, so Gradle builds never noticed.
Explicit environment and bwrap.env entries can still override.
(Werkdock's own engine is immune by design: it runs --clearenv.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--exclude=sys matched every path component named sys anywhere in the
tree — GNU tar applies slash-less patterns to all components — and so
silently dropped usr/share/go-1.24/src/internal/runtime/sys from the
archive, breaking every go build in the sandbox with 'package
internal/runtime/sys is not in std'. The excludes are now anchored
(./proc, ./sys, ./dev), so only the top-level mountpoints stay out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run-time composition instead of baked-in toolchain combinations: a slim
base image plus per-toolchain prefix binds (--with jdk-21 --with go-1.24),
possible because official toolchain tarballs live under their own
prefixes — plain ro-binds, no overlayfs, no root, today's bwrap.
Comes due when a second toolchain combination is needed; until then the
one fat image stays the deliberate choice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flat images without layers (~2 GiB unpacked for a fat JDK+Go+Node
buildenv), near-free instances (read-only rootfs bind + tmpfs), the
one-fat-image recommendation, the orphaned-environment trap on archive
path changes, and the future overlay/hardlink-dedup options.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second named build beside the default: gofmt gate, go vet, go test,
and the static binary as the artifact (werkdock/dist). Verified locally
end to end and via config:print.
Expected red on the webspace instance until the trixie rootfs learns
the go toolchain — that image extension (Go + Node, roadmap step 2 of
plan 21) is the follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The minimal build-capable CLI decided in RFC 0002's outcome: stdlib-only
Go module, one static binary.
- engine: RunSpec behind the Engine interface (RFC 0001); the Bwrap
engine ports Werkator's hardened invocation — uid-0 mapping, read-only
rootfs at /, proc/dev/tmp/root before the user binds so binds below
them land inside, mountpoint pre-creation in the rootfs including file
mountpoints, and a guard against binds escaping the rootfs. --clearenv
gives docker-style clean environments (HOME/PATH set explicitly).
- store: images under $WERKDOCK_HOME (default ~/.werkdock), load
unpacks via the tar CLI into a tmp dir and renames atomically.
- cli: docker-shaped run flags (-v/-e/-w/--rm); refused docker flags
(-p, --network, --memory, --cpus, --user, -d) fail loudly with the
reason; exit codes follow docker (125 CLI errors, child code through).
- doctor: port of werkator-build-prerequisites.sh — userns probe with
the three signals, tar/zstd, free space and group-quota headroom via
testable df/quota parsers, same PASS/FAIL output.
- tests: argv golden test, mountpoint and escape tests, flag refusals,
store round trip, doctor parsers — plus real-sandbox integration
tests that skip where bwrap or userns are unavailable.
Also records in step 21: RFC 0002 levels 2/3 deferred; next goal is
sandbox builds of Werkator, Werkbaum, and Werkdock itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Level 1: docker-compatible CLI (verbs, flags, loud refusal of isolation
flags the filesystem-only contract cannot honor) — built in session B.
Level 2: OCI image pull, flattened to a rootfs — deferred, stdlib-doable.
Level 3: a daemon speaking the Docker Engine API subset Testcontainers
actually uses (Testcontainers never calls the CLI) — deferred, but the
CLI is built as a thin frontend over the same internal service from the
start. Records the port-mapping crux of host networking and the
unprivileged-netns escape hatch as a future RFC.
Step 21 session B and the werkdock README follow the docker-shaped
semantics: run takes an image, instances correspond to containers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full five-way evaluation (bash, Python 3, Kotlin Native, Rust, Go)
with the numeric scoring, the Kotlin Native deep-dive, and the
own-namespaces-instead-of-bwrap analysis, ending in a concrete proposal:
Go, stdlib-only, one static binary, sandbox engine behind an interface
so bwrap can later be swapped for native namespaces.
Status proposed — the decision outcome is recorded once made.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PR-docs get their real numbers: bwrap-build-runtime -> PR#4,
build-current-head-from-the-branches-view -> PR#3 (files and scenario IDs)
- tools/remote: header note marking install's clone step and build as the
self-build prototype, superseded by session D of step 21 (usage range grown)
- ADR 0008: bubblewrap user-namespace sandbox as the third build runtime;
step 17 and the plan README now point at 0008 (0007 was taken by build
definitions before the step landed)
- AGENTS.md: decisions list catches up with ADR 0007 and ADR 0008
- architecture skill: BwrapBuildRunner paragraph (rootfs unpack, uid mapping,
mount ordering, pinned keys) and the dispatcher's three-way routing
- plan README: step 21 entry now describes the werkdock/ subdirectory path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records where the bwrap work (PR #4) drifted from the intent of ADR 0006 —
the webspace self-build instead of local-build-plus-install — and breaks the
correction into four sessions: close step 17's open ends, bootstrap Werkdock,
let Werkator consume it, replace the self-build with the bundle install path.
The extracted sandbox tool is named Werkdock (decided after three naming
rounds, rationale and dropped candidates in the step file). It grows in the
werkdock/ subdirectory, seeded here with its README, and moves to its own
repository once it stands on its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add the bubblewrap build runtime (step 17, ADR 0007)
BwrapBuildRunner: third runtime behind BuildRunner for hosts without root
and without Docker (e.g. Hostsharing managed webspaces). Shells out to the
bwrap CLI, unpacks a prepared rootfs on demand into
.git/werkator/buildenv/<envKey>/rootfs, reuses the Docker runner's git
metadata mounts, and returns the attached bwrap process for streaming and
cancellation.
Config: bwrap.enabled/rootfs/env on BranchConfig and BwrapOverrides on
BuildDefinition; enabled/rootfs are pinned like the docker sandbox policy.
Docker and bwrap are mutually exclusive per build, rejected in
buildSettings instead of picked silently. DispatchingBuildRunner routes
bwrap; InitCommand template, docs/configuration.md and AGENTS.md in sync.
* bwrap rollout tooling: remote script, prerequisites disk/quota check, absolute workspace binds
- tools/remote: central remote control script with check-prerequisites, install and build commands
- tools/werkator-build-prerequisites.sh: compact PASS/FAIL output, target-dir parameter, free-space and
group-quota headroom checks against the ~5 GiB build footprint, home-filesystem reference
- BwrapBuildRunner: bind workspace and home at absolute paths resolved against repoDir — a relative
path made bwrap create mountpoints inside the read-only rootfs (seen on the webspace); regression test
- TestcontainersSmokeTest: gated with enabledIf docker available (skip, never fail, without a daemon)
- docs: configuration reference, step-17 plan notes, PR-doc
* bwrap: bind the repo read-write before the workspace so mountpoints are creatable
bwrap creates mountpoints for bind destinations inside the sandbox; with only a
read-only rootfs bound at /, creating them for the workspace under .git/werkator/
worktrees failed with 'Read-only file system' (seen on the webspace). Binding the
repo dir read-write first provides the base; the git metadata mounts then layer
the usual isolation on top (read-only .git, tmpfs mask over .git/werkator,
read-write worktree admin dir).
* bwrap: pre-create bind mountpoints inside the unpacked rootfs
bwrap mkdirs mountpoints for bind destinations against the sandbox view; with the
rootfs ro-bound at / every destination missing from the rootfs (the repo dir under
/home/storage/... on the webspace) fails with 'Read-only file system'. The rootfs
directory is a plain host dir, so create the mountpoints there before launching
bwrap; it then finds them and has nothing left to create.
* bwrap: skip existing rootfs files when pre-creating bind mountpoints
/etc/resolv.conf is a file the rootfs already ships; createDirectories threw on it.
Only missing directories are created now.
* bwrap: pre-create proc/dev/tmpfs mountpoints in the rootfs too
The rootfs archive ships no /proc or /dev (excluded when packed), so bwrap failed
mkdir'ing their mountpoints against the read-only root.
* bwrap: bind the workspace after the git metadata mounts
The tmpfs mask over .git/werkator shadowed the earlier workspace bind, because the
worktree lives under .git/werkator/worktrees — chdir then failed with ENOENT. The
workspace bind now comes last and shadows the mask at exactly its own path.
* systemd resource limits and webspace start command (step 17, web access)
- server.systemd.memoryMax/tasksMax (empty = directive omitted): on platforms where
the service runs in a shared memory slice (Hostsharing Managed Webspaces) a
runaway Gradle build must not starve the whole package; init --systemd reads the
effective config and bakes the values into the generated unit
- tools/remote werkator start: writes server settings (assigned port, loopback
bind, publicBaseUrl, nginx off) plus the Apache reverse-proxy .htaccess into
~/doms/<domain>/subs/www, runs init --systemd and enables the user unit
- docs/configuration.md documents the new keys
* tools/remote: env-based configuration and background port-forward
All connection and deployment values come from .env in the repository root
(WERKATOR_REMOTE, WERKATOR_PATH, WERKATOR_PORT, WERKATOR_DOMAIN,
WERKATOR_LOCAL_PORT, optional WERKATOR_BRANCH/MEMORY_MAX/TASKS_MAX/ROOTFS);
missing values fail with a pointing error instead of positional parameters.
- port-forward is now 'tools/remote port-forward start|stop' with a detached
ssh tunnel, pid file under /tmp, and idempotent start
- start restarts the systemd unit after updating the machine config
- control-token generates the token in place when the server has not yet
- the rootfs archive default moves to build/ (already gitignored)
* renaming from gitTally to Werkator
* Rename GitTally to Werkator
`gitTally` is the name of another product in the git space, so the
rename is a precaution; nothing about what the build system does changes.
The name follows one rule: `Werkator` where it is prose, capitalized
where it is a Kotlin type and its file, lowercase everywhere a machine
reads it — the command, packages, paths, configuration keys and values,
the Gitea check context. Environment variables keep their convention and
are uppercase throughout.
Every configuration file is still found under its pre-rename name
(`ConfigFiles`): `.gittally.yml` at the repository root, in a build
worktree and as committed on a branch, `.git/gittally/.gittally.yml` for
the machine layer. The current name wins where both exist, and the old
file is then ignored rather than merged — two files side by side are a
half-done rename, not a layering. Without the fallback an installation
that updated without renaming would not fail: a configuration that is
not found leaves every setting at its default, so it would come up
looking healthy while having forgotten its credentials and its builds.
`docs/werkator-migrationsplan.md` lists what the fallback does not
cover and has to be moved by hand — above all the state directory
`.git/werkator/`, which holds the build history, the control token and
the worktrees, and has no fallback of its own.
`docs/migration-from-legacy.md` is deleted with this: it mapped the
legacy script's environment variables, and every host it addressed has
long since moved to the YAML configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Move the pre-rename state directory at the first start
The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.
Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.
The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.
Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Document PR#1: the rename to Werkator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the legacy env-to-YAML conversion from the setup tool
The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.
The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.
It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.
`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Describe this repository's build with a build definition
Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.
Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Stop documenting the pre-rename fallback for users
Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.
Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Bring the PR-doc to its final state
The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.
Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Release v1.0.0: Werkator
The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Correct the PR-doc about the version
It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Point the legacy references at the history
`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.
The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Retarget the links in the historic PR-docs
The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add the v1.0.0 deployment procedure for vm4006
Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.
Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Rename the machine configuration along with its directory
Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.
`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let init see a configuration under its previous name
`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.
Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Record the v1.0.0 deployment to vm4006
Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the control token left under the old localStorage key
The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.
Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Plan tracking the build duration over time (step 20)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* renaming from gitTally to Werkator
* Rename GitTally to Werkator
`gitTally` is the name of another product in the git space, so the
rename is a precaution; nothing about what the build system does changes.
The name follows one rule: `Werkator` where it is prose, capitalized
where it is a Kotlin type and its file, lowercase everywhere a machine
reads it — the command, packages, paths, configuration keys and values,
the Gitea check context. Environment variables keep their convention and
are uppercase throughout.
Every configuration file is still found under its pre-rename name
(`ConfigFiles`): `.gittally.yml` at the repository root, in a build
worktree and as committed on a branch, `.git/gittally/.gittally.yml` for
the machine layer. The current name wins where both exist, and the old
file is then ignored rather than merged — two files side by side are a
half-done rename, not a layering. Without the fallback an installation
that updated without renaming would not fail: a configuration that is
not found leaves every setting at its default, so it would come up
looking healthy while having forgotten its credentials and its builds.
`docs/werkator-migrationsplan.md` lists what the fallback does not
cover and has to be moved by hand — above all the state directory
`.git/werkator/`, which holds the build history, the control token and
the worktrees, and has no fallback of its own.
`docs/migration-from-legacy.md` is deleted with this: it mapped the
legacy script's environment variables, and every host it addressed has
long since moved to the YAML configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Move the pre-rename state directory at the first start
The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.
Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.
The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.
Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Document PR#1: the rename to Werkator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the legacy env-to-YAML conversion from the setup tool
The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.
The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.
It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.
`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Describe this repository's build with a build definition
Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.
Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Stop documenting the pre-rename fallback for users
Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.
Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Bring the PR-doc to its final state
The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.
Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Release v1.0.0: Werkator
The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Correct the PR-doc about the version
It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Point the legacy references at the history
`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.
The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Retarget the links in the historic PR-docs
The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add the v1.0.0 deployment procedure for vm4006
Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.
Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Rename the machine configuration along with its directory
Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.
`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let init see a configuration under its previous name
`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.
Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Record the v1.0.0 deployment to vm4006
Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the control token left under the old localStorage key
The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.
Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Build the branch's current head from the branches view
A row on /branches stands for a branch, not for a past run, so its
restart button now builds what origin has now (`atOriginHead`). Repeating
an overtaken commit answers a question nobody asked, and a build gate
comparing HEAD against origin — hsadmin-ng's `prQuickCheck` — cannot even
pass on it: the retry of a superseded master commit is a guaranteed red.
Latest and history are unchanged. There a row *is* a recorded run, and
repeating it means that commit.
The row keeps its build definition either way: a retry of `main@pitest`
runs the new commit as `pitest`, on the branch the record names. A branch
that is gone from origin is refused by name instead of silently falling
back to the old commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Document the branches-view restart (PR#000)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Release v1.0.1: the branches view builds the current head
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* renaming from gitTally to Werkator
* Rename GitTally to Werkator
`gitTally` is the name of another product in the git space, so the
rename is a precaution; nothing about what the build system does changes.
The name follows one rule: `Werkator` where it is prose, capitalized
where it is a Kotlin type and its file, lowercase everywhere a machine
reads it — the command, packages, paths, configuration keys and values,
the Gitea check context. Environment variables keep their convention and
are uppercase throughout.
Every configuration file is still found under its pre-rename name
(`ConfigFiles`): `.gittally.yml` at the repository root, in a build
worktree and as committed on a branch, `.git/gittally/.gittally.yml` for
the machine layer. The current name wins where both exist, and the old
file is then ignored rather than merged — two files side by side are a
half-done rename, not a layering. Without the fallback an installation
that updated without renaming would not fail: a configuration that is
not found leaves every setting at its default, so it would come up
looking healthy while having forgotten its credentials and its builds.
`docs/werkator-migrationsplan.md` lists what the fallback does not
cover and has to be moved by hand — above all the state directory
`.git/werkator/`, which holds the build history, the control token and
the worktrees, and has no fallback of its own.
`docs/migration-from-legacy.md` is deleted with this: it mapped the
legacy script's environment variables, and every host it addressed has
long since moved to the YAML configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Move the pre-rename state directory at the first start
The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.
Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.
The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.
Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Document PR#1: the rename to Werkator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the legacy env-to-YAML conversion from the setup tool
The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.
The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.
It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.
`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Describe this repository's build with a build definition
Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.
Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Stop documenting the pre-rename fallback for users
Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.
Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Bring the PR-doc to its final state
The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.
Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Release v1.0.0: Werkator
The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Correct the PR-doc about the version
It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Point the legacy references at the history
`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.
The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Retarget the links in the historic PR-docs
The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add the v1.0.0 deployment procedure for vm4006
Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.
Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Rename the machine configuration along with its directory
Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.
`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let init see a configuration under its previous name
`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.
Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Record the v1.0.0 deployment to vm4006
Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the control token left under the old localStorage key
The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.
Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Also fixes the log-capture test added with step 19: the test JVM logs at
WARN, so the INFO line reporting the recovered fetch never reached the
appender. The helper now lowers the level explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A watcher that cannot fetch leaves the server perfectly reachable and
every branch row stale — a calm list that implies nothing changed. The
state was already recorded and served at /api/watcher; only nothing
rendered it.
The banner lives in the shared nav fragment, so every view inherits it,
and is fed from the view's own polling tick without being chained to it.
It stays separate from live-indicator: that one reports whether the
browser reaches the server, this one whether the server reaches origin.
Also caps the log volume the outage exposed: a lasting fetch failure is
logged when its message changes, not on every cycle, and the recovery is
logged once. Same for an invalid atTimes slot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A wrong git token made the watcher fail every fetch for 57 minutes while
the branches view kept showing its last known list. WatcherState already
records lastFetchError and /api/watcher already serves it; only the UI
never renders it.
The step also folds in the logging volume: one outage wrote 297 identical
warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinning is one rule — strip the key from the branch layer — but the keys
fall into two groups by where they are meant to live: what only the
machine can know, and what belongs in the repository yet must not be
decided per branch. docker.enabled/network moves from the first group to
the second once the committed config carries it.
The KDoc says explicitly that the distinction is documentary, so nobody
looks for two mechanisms in stripPinned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not a shrink to the pinned docker keys but a full removal: the pinning
strips the branch layer only, so master's committed config is merged
unstripped and its sandbox policy reaches every branch. The nightly job
moves there with it. A rollback below v0.9.19 is no longer provided for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The machine config on vm4006 lost its legacy branches block, renamed
builds.master to builds.nightly and moved build/libs into the default
definition's artifactDirs. Step 18 still described that work as pending
and named a rollback path to 0.9.18 that the file no longer supports.
What is left there is the shrinking to genuinely host-specific keys,
plus the open question where the nightly rebuild belongs once master
carries its own config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`onPush`, `atTimes`, `branches`, and `activeWithin` move into a nested
`trigger`. The split is structural on purpose: the inheritance from
`builds.default` now subtracts one key instead of a list of four, so a
selector added to `TriggerConfig` later is non-inheritable by
construction rather than because someone remembered to extend the list.
A definition still writing those keys flat is refused by name, per file
and scoped like the version check — the machine and project config abort
the start, a branch's committed config fails only that branch. Ignoring
them would leave the build with no trigger at all, which is a job that
quietly stops running: the failure this refusal exists to prevent.
Two more things a definition can now say:
- A `!` prefix in `trigger.branches` excludes, and an exclusion wins
whatever the order. `["*", "!master"]` gives one branch a build of its
own without the default build running over it as well — until now the
only way out of that double build was to drop the second definition's
push trigger.
- `statusContext` overrides the Gitea check this build reports as, empty
keeping the repository-wide one. Two builds of a commit shared a
context and overwrote each other's result, so a quick check beside a
long build was not readable in Gitea. Pinned like `requirePullRequest`:
a branch that could pick its context could take over the check a branch
protection rule depends on.
Fixed on the way: a branch whose builds all belong to named definitions
rendered an empty row in the branches view, reading as "never built"
directly beside its real builds. That row was unreachable before the
exclusion patterns made such a branch possible.