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
9.6 KiB
Werkator — Agent Instructions
This file holds the shared, tool-agnostic instructions for all AI coding agents.
Claude Code imports it from CLAUDE.md via @AGENTS.md; Claude-Code-specific instructions belong in CLAUDE.md, everything else here.
Detailed guides live as Agent Skills under .claude/skills/ (SKILL.md format); they load on demand — see Skills.
Build and Test Commands
./gradlew build # compile + ktlintCheck + test
./gradlew ktlintFormat # auto-format before committing
./gradlew test # run all tests (can be slow, prefer single test)
./gradlew test --tests "de.hoennig.werkator.ApplicationContextTest" # example for running a single test class
Run the JAR directly:
java -jar build/libs/werkator.jar --help
java -jar build/libs/werkator.jar init
ktlintFormat must be run before build passes — the formatter is enforced as part of the check lifecycle.
Architecture Overview
Werkator is a lightweight, declarative CI/CD build system: git-centric, one instance per repository, builds native or in Docker, statuses reported to Gitea. It is a dual-mode application: CLI (interactive, status, config) and Server (HTTP, persistent web UI + JSON API). IMPORTANT: Before designing or modifying code in any production package, load the architecture skill — it holds the subsystem details (CLI wiring, server mode, web UI, config system, git access, build execution, watcher, metrics).
Package Structure
All production code lives under de.hoennig.werkator, with sub-packages commands (picocli subcommands), config (YAML config loading and schema), git (git CLI access), gitea (Gitea commit-status API client), build (build execution, results, workspaces), artifacts (filesystem artifact store), watcher (branch polling, auto-builds, startup recovery), metrics (system resource sampling and aggregation), and server (JSON API controllers, Thymeleaf UI, artifact serving, control token, watcher and metrics lifecycles). Tests mirror this structure under src/test/kotlin.
Hard Invariants
exitProcessis called only frommain()— never insideCliRunner.run(); this keeps the Spring context alive during tests.- Nothing is scheduled during CLI runs or tests: the watcher poll loop and metrics sampling start only via an explicit
start()in theserverprofile. - Builds run detached in worktrees under
.git/werkator/worktrees/<branchKey>; the primary checkout is never used for builds; never assume a single running build. - When config keys change, three places must stay in sync: the
WerkatorConfigdata classes, theInitCommandtemplates, anddocs/configuration.md. - Every config file may declare
werkator.version.since/below(the Werkator it is written for, never a format version — no API is involved).sinceis enforced in both directions, usingConfigVersions.FORMAT_BROKE_INfor "file predates a breaking change";belowonly warns. A violation aborts the start for the machine and project config, but fails only that branch's builds for a branch config. - A branch describes its own CI: its committed
.werkator.ymlis the branch layer (ConfigLoader.loadWithBranchLayer, used by the watcher per origin branch and byloadForWorktreeat build time) and takes precedence over.git/project — including the wholebuildssection, so a new configuration can be tried out on a branch without affecting other branches. Only the pinned set is stripped from that layer: secrets (git), host/repository sections (server,gitea,executor,watcher), the docker (docker.enabled,docker.network) and bubblewrap (bwrap.enabled,bwrap.rootfs,bwrap.werkdock) sandbox policies, and the trust gate (requirePullRequest). A branch must never reach credentials, disable its container or sandbox, change its network, substitute a foreign rootfs, raise global concurrency, or bypass its own pull-request gate; a branch's definitions apply to that branch alone. - A build definition carries the complete description of its build, split in two: the
triggerblock (onPush,atTimes,branches,activeWithin) says when and for which branches it runs, everything else what it does.builds.defaultis the base every other definition inherits its settings — never itstrigger— from. The split is structural so that a selector added toTriggerConfiglater is non-inheritable by construction; writing a trigger key flat is refused, never ignored, because ignoring it leaves a build that silently stops running. A!prefix intrigger.branchesexcludes and always wins. - The inheritance is applied after all layers are merged: that order is what makes a build a branch invents inherit the host's sandbox policy instead of the data-class default, so the pinning also holds for a build the host has never heard of. Pinned are
requirePullRequest,statusContext,docker.enabled,docker.network,bwrap.enabled,bwrap.rootfs, andbwrap.werkdock. Docker and bwrap are mutually exclusive per branch — enabling both is rejected at start. buildsor the legacybranches, never both:branchesis read only while the merged config defines no build at all (builds.maxConcurrentis not one), and ignored with a warning as soon as one exists. The section is deprecated and goes away once the repositories have migrated; thenConfigVersions.FORMAT_BROKE_INgets set and a leftoverbranches:key must be rejected by name — the version check alone cannot catch a file that declares no version.- Web UI: server-rendered Thymeleaf plus one hand-written
static/werkator.js— no SPA framework, no frontend build pipeline; every fetch has a timeout and an explicit error badge;UiFormatsandwerkator.jsmust produce identical display formats. - Git and Docker access shells out to the CLIs (
GitCommandRunner,docker) — no JGit, no Docker SDK.
Testing
Tests use Kotest FunSpec with MockK; SpringExtension is registered globally in io.kotest.provided.ProjectConfig — do not add it per-spec.
IMPORTANT: Before writing or changing tests, load the writing-tests skill — it holds the spec structure and the Spring-slice mocking patterns.
File-Formatting
Markdown
Write documentation in English in Markdown files. In Markdown, use a single line per sentence. Keep sentences short.
Documentation
docs/Werkator-Konzept.md— product concept and target architecture (in German): git-centric CI, builds in Docker, one instance per repository, status reported back to Gitea.docs/configuration.md— configuration reference; keep in sync withWerkatorConfigand theinittemplates.docs/bootstrapping.md— howinitprepares a repository.docs/deployment.md— running Werkator as a systemd user service behind an existing reverse proxy (init --systemdgenerates the unit).docs/werkator-migrationsplan.md— renaming a running installation from GitTally to Werkator: what the name fallback covers and what has to be moved by hand.docs/plan/— the step-by-step rewrite plan;docs/plan/README.mdexplains how to execute a step,docs/plan/00-legacy-analysis.mdsummarizes the legacy bash script.docs/prs/— one document per pull request; every PR needs one. IMPORTANT: Before opening or finishing a pull request, load the pr-doc skill and write the PR-doc.
Key Architectural Decisions
All major decisions are in docs/adrs/. Run adr-status (after source .envrc) for a one-line summary of each. Decisions in force:
- Test framework: Kotest + MockK + WireMock + Testcontainers (ADR 0001)
- Gradle: 8.14.5 (ADR 0002)
- Spring Boot: 4.0.6 (ADR 0003)
- Rewrite architecture: JSON-file persistence behind a repository interface, server-rendered UI with JSON polling, no managed nginx — systemd unit behind the host's reverse proxy (ADR 0004)
- Managed nginx/TLS: revises ADR 0004 — an opt-in nginx+certbot container for hosts without a reverse proxy (e.g. Hostsharing), planned as
docs/plan/13-nginx-tls.md(ADR 0005) - Runtime bundle distribution:
./gradlew runtimeBundlebuilds a jlink-trimmed JRE + jar tarball for hosts without a Java runtime; GraalVM native image and a containerized runtime were rejected (ADR 0006) - Build definitions: a top-level
buildssection of named builds withtriggerblocks replaces the branch-ownedautoBuildschedules (ADR 0007) - bwrap build runtime: on hosts without root and without Docker (Hostsharing Managed Webspaces), builds run in a
bwrapuser-namespace sandbox over a prepared rootfs — filesystem isolation only, network and uid shared with the host; proot/fakechroot and unisolated native builds were rejected (ADR 0008) - Multi-repo instance: one instance serves a registry of self-contained repositories (instance config in
~/.werkator.yml, repo config in each repo); revises the one-instance-per-repository tenet, implementation planned asdocs/plan/22-multi-repo.md(ADR 0009)
Skills
On-demand guides in the cross-tool SKILL.md format; agents with skill support load them automatically by description, all others should read the linked files when the topic comes up:
- architecture — subsystem details: CLI wiring, server mode, web UI, config, git access, build execution, watcher, metrics.
- writing-tests — Kotest/MockK conventions and Spring slice-test mocking patterns.
- pr-doc — how to write the mandatory per-PR documentation in
docs/prs/.