A build definition may declare afterSuccessOf, running whenever the named build of the same branch turns green — the deployment path chosen over a deployCommand or a separate deploy section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #23
539 lines
37 KiB
Markdown
539 lines
37 KiB
Markdown
# Werkator Configuration Reference
|
|
|
|
Werkator is configured via YAML files. Settings are merged from several sources in order — later layers override earlier ones.
|
|
|
|
## Config File Locations
|
|
|
|
| Layer | Path | Committed to Git | Purpose |
|
|
|--------------------------|----------------------------|------------------|----------------------------------------------|
|
|
| Instance config | `~/.werkator.yml` | No | The repository registry and the instance-level settings; optional `defaults` below every repository (see below) |
|
|
| Project config | `.werkator.yml` | Yes | Shared team settings |
|
|
| Applied instance fragment | `.git/werkator/.werkator.applied.yml` | No | Instance parameters installed by `init --apply` |
|
|
| Repo installation config | `.git/werkator/.werkator.yml` | No | Machine- or user-specific overrides, secrets |
|
|
| Branch config | `.werkator.yml` committed on a branch | Yes | That branch's build settings and build definitions |
|
|
|
|
The repo install config (`.git/werkator/.werkator.yml`) wins on any key present in several files; the applied fragment wins over the project config. Typically the repo install config sets `git.token` and `git.account` without committing them.
|
|
|
|
### The applied instance fragment (`init --apply`)
|
|
|
|
`werkator init --apply FILE` installs a YAML fragment in this very schema as its own layer (step 23) — the file a deployment wrapper hands over instead of patching configs.
|
|
The fragment is validated strictly before installing: an unknown key is refused loudly, never ignored, because a typo would otherwise install a value that silently does nothing.
|
|
It is then copied verbatim (comments included) to `.git/werkator/.werkator.applied.yml`; re-applying replaces the file, so nothing accumulates or duplicates, and the hand-edited repo install config — which always wins — is never rewritten.
|
|
|
|
### Which Werkator a file is written for
|
|
|
|
Every configuration file may declare the Werkator it was written for. Without it, a
|
|
version that renames or drops a key does not fail — it silently ignores what it no longer
|
|
understands, and the effect shows up as a build that does the wrong thing.
|
|
|
|
```yaml
|
|
werkator:
|
|
version:
|
|
since: "0.9.16" # enforced: an older Werkator refuses to read this file
|
|
below: "2.0" # your release marker; Werkator decides how strictly to take it
|
|
```
|
|
|
|
There is deliberately **no version of the file format** (no `apiVersion`): no API is
|
|
involved — Werkator reads its own configuration — and only one configuration generation is
|
|
ever supported. The declaration exists to make an incompatibility nameable, never to run
|
|
two parsers.
|
|
|
|
`since` is a hard floor and covers both directions:
|
|
|
|
- a newer file on an older Werkator is refused instead of being half-understood;
|
|
- a file written *before* a breaking change and read *after* it is refused as well —
|
|
Werkator knows in which version its configuration format last broke, so the message can
|
|
name the change: *"is written for Werkator 1.4.0, but the configuration format changed
|
|
incompatibly in 2.0.0: `builds:` is now `buildSpec:`"*.
|
|
|
|
`below` is optional and names the first version this file was **not** released for. The
|
|
bound is exclusive, so `below: "2.0"` means everything up to 2.0.0. On its own it only
|
|
warns — a caution marker nobody maintained must never stop a CI. The refusal above comes
|
|
from Werkator's own knowledge of its breaking changes, not from this value. The intended
|
|
routine is the one known from IDE plugins: a new version appears, the warning shows up, you
|
|
try it (on a test host, or in production with a rollback ready), and then raise `below` and
|
|
commit that.
|
|
|
|
A file that declares nothing is read as before, with a hint in the log — a missing line
|
|
must never stop a server either. `werkator init` writes the running version into the
|
|
generated config.
|
|
|
|
How far a violation reaches depends on the file, following the same rule as everything
|
|
else here: the machine and project configs abort the start (the message names the file and
|
|
the rollback), while an incompatible **branch** config fails only the builds of that
|
|
branch. A branch that was cut before a migration must never stop the server or hold up the
|
|
branches that are fine.
|
|
|
|
### The branch layer: a branch describes its own CI
|
|
|
|
The `.werkator.yml` committed on a branch is applied as a third layer on top of the two
|
|
above, giving the precedence **branch > repo install > project**. It takes precedence for
|
|
everything that describes how this branch is built: the whole `builds` section — its own
|
|
definitions and its overrides of the definitions from the project config, with
|
|
`buildCommand`, `cleanCommand`, `artifactDirs`, log file names, and
|
|
`docker.image`/`dockerfile`/`context`/`env` and `werkdock.env` inside them. That is how a new configuration is tried out: change it on a branch, and
|
|
no other branch's builds are affected.
|
|
|
|
The branch layer is used in both places where it matters: the watcher reads the committed
|
|
config of each origin branch (via `git show`, only when the branch moved) to decide which
|
|
of *its* builds are due, and the build itself resolves its settings from the worktree of
|
|
the commit being built.
|
|
|
|
A branch's definitions apply to that branch alone. Their selectors are evaluated for it
|
|
only, so a definition committed on one branch can never trigger builds of another — even
|
|
when its `branches` selector names one.
|
|
|
|
A pinned set is always taken from the repo install/project config, because none of it
|
|
describes this branch's build.
|
|
Which of those two files is expected to carry a key gives the two names used throughout
|
|
this documentation.
|
|
|
|
**host-pinned** — only the machine can know it, and it never belongs in the repository:
|
|
|
|
- secrets: the whole `git` section;
|
|
- the host's own addresses and ports: the whole `server` section.
|
|
|
|
**master-pinned** — it belongs in the repository, where changing it needs a review, but no
|
|
single branch may decide it:
|
|
|
|
- the repository-side settings: the whole `gitea`, `executor`, and `watcher` sections;
|
|
- the trust gate: `requirePullRequest`, and the Gitea status context: `statusContext`;
|
|
- the container sandbox policy: `docker.enabled`/`docker.network` and
|
|
`werkdock.enabled`/`werkdock.rootfs`/`werkdock.binary` — host-pinned as
|
|
long as only the host's configuration sets them, master-pinned once the committed
|
|
configuration does;
|
|
- the trigger of a [follow-up build](#follow-up-builds): `trigger.afterSuccessOf` in every definition, and the whole `trigger` block of a definition the host defines as a follow-up.
|
|
A branch may say what its deployment does, never that — or for which branches — it happens.
|
|
|
|
The distinction is documentary.
|
|
Werkator applies one rule: every pinned key is stripped from the branch layer, and the
|
|
value then resolves from whichever remaining layer sets it.
|
|
The names say where a key is meant to live, not how it is enforced.
|
|
|
|
This keeps a branch from reaching credentials, reporting statuses to another repository,
|
|
raising the global concurrency, disabling its own build container, changing its network
|
|
mode, bypassing its own pull-request gate, or deploying itself. Everything else is the branch's to decide —
|
|
it can already run any command through `buildCommand`.
|
|
The pinned settings are stripped wherever they appear, in a build definition as well as in
|
|
a legacy `branches` entry. The deprecated `branches` section itself is read from the repo
|
|
install/project config only, and only while nothing defines a build at all.
|
|
|
|
## `~/.werkator.yml` — the instance configuration
|
|
|
|
One Werkator instance serves a *set* of repositories (ADR 0009): one service, one port, one UI, one watcher loop.
|
|
The set and everything shared by it live in `.werkator.yml` in the home directory of the user running Werkator — one instance per OS user.
|
|
The file name is deliberately the same everywhere; only the location carries the meaning: home is the instance, the repository root is the project, `.git` is the machine.
|
|
`WERKATOR_HOME` overrides the directory the file is looked up in.
|
|
|
|
Without this file Werkator serves the current working directory exactly as before.
|
|
With it, the registry wins over the current directory: `werkator server` serves the registered repositories wherever it is started.
|
|
|
|
```yaml
|
|
werkator:
|
|
version:
|
|
since: "0.9.16" # like every other config file
|
|
|
|
repositories: # the registry; each entry is one served repository
|
|
- path: ~/repos/werkator # absolute, or relative to the home directory (~ expands)
|
|
- path: ~/repos/werkbaum
|
|
name: baum # optional; default is the directory basename
|
|
|
|
server: # the instance's server section — port, bind address, public URL, nginx
|
|
port: 18080
|
|
executor:
|
|
maxConcurrent: 2 # the global cap over all repositories
|
|
watcher:
|
|
pollInterval: 10s # one loop, one delay
|
|
|
|
defaults: # optional: repository-level keys merged BELOW every repository's own layers
|
|
git:
|
|
account: ci-bot
|
|
token: "…" # secrets may then live here instead of in each repository
|
|
gitea:
|
|
baseUrl: https://git.example.org
|
|
```
|
|
|
|
Key ownership once this file exists:
|
|
|
|
- **Instance-level** — read from this file alone: the whole `server` section, `executor.maxConcurrent`, and `watcher.pollInterval`.
|
|
A repository file still carrying one of them is ignored on that key, with one warning naming both files; it is never merged silently.
|
|
- **Repository defaults** — the `defaults` block, in the repository config schema: merged below each repository's project config, applied fragment, machine config, and branch layer, so a repository's own value always wins.
|
|
Pinning is unchanged: home defaults and the repository's machine config are both host-side layers, and a branch still cannot reach a pinned key.
|
|
- **Repository-level** — everything else stays in the repository's own files: `gitea.*`, `git.*` credentials, `builds`, retention, the other `watcher` keys (`pullRequestGate`, `newBranchMaxAge`, `fastForwardLocalRefs`), and the sandbox policy.
|
|
|
|
Repository names are the `--repo` selector of `werkator build`, `retry`, and `status` (without it a command means the current directory when served, otherwise the first registered repository) and will become the route segment of the web UI.
|
|
Two entries resolving to the same name abort the start, as does an entry that is not a git repository.
|
|
A repository whose configuration this Werkator must not read (see the version declaration) is skipped with an error; the others are served.
|
|
|
|
## Inspect the Effective Config
|
|
|
|
```bash
|
|
java -jar build/libs/werkator.jar config:print # only explicitly set values
|
|
java -jar build/libs/werkator.jar config:print --full # all values including defaults
|
|
```
|
|
|
|
`git.token` is masked as `***` by default, so the output can safely be shared or pasted.
|
|
Add `--show-secrets` to print it in clear text.
|
|
|
|
## `.werkator.yml`
|
|
|
|
Values shown are the defaults.
|
|
|
|
```yaml
|
|
# The Werkator this file is written for (see the section above).
|
|
werkator:
|
|
version:
|
|
since: "0.9.18" # enforced: older Werkator refuses this file
|
|
below: "2.0" # optional release marker; warns, does not block
|
|
|
|
server:
|
|
# Public base URL of this Werkator installation — used for all links posted to Gitea.
|
|
publicBaseUrl: https://ci.example.org/
|
|
# HTTP port of the `server` subcommand (default 18080, like legacy)
|
|
port: 18080
|
|
# bind address of the `server` subcommand; loopback only, because the UI and the API
|
|
# are unauthenticated — set 0.0.0.0 only deliberately (see the note below)
|
|
bindAddress: 127.0.0.1
|
|
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
|
impressumUrl: ""
|
|
# Resource limits of the systemd user unit generated by `init --systemd`; empty = directive omitted.
|
|
# Needed where the service shares a memory slice, e.g. Hostsharing Managed Webspaces, where a
|
|
# runaway Gradle build would starve everything else in the package.
|
|
systemd:
|
|
memoryMax: "" # e.g. 1G — written as `MemoryMax=` into the unit
|
|
tasksMax: "" # e.g. 512 — written as `TasksMax=` into the unit
|
|
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
|
|
# a usable reverse proxy (ADR 0005; see notes below and deployment.md).
|
|
nginx:
|
|
# manage an nginx Docker container with Let's Encrypt certificates
|
|
enabled: false
|
|
# public DNS name served by nginx and used for the certificate; required when enabled
|
|
serverName: ""
|
|
# host port published as nginx port 80 (ACME challenge + HTTPS redirect)
|
|
httpPort: 8080
|
|
# host port published as nginx port 443
|
|
httpsPort: 8443
|
|
# host nginx proxies to; empty = serverName (the container cannot reach localhost)
|
|
upstreamHost: ""
|
|
# name of the managed container; empty = werkator-nginx-<repo-name>
|
|
containerName: ""
|
|
# directory for nginx config, certificates, and logs;
|
|
# empty = $XDG_STATE_HOME (or ~/.local/state) plus /werkator/nginx/<repo-key>
|
|
stateDir: ""
|
|
# e-mail for the Let's Encrypt account; empty registers without one
|
|
letsencryptEmail: ""
|
|
|
|
# Gitea integration for fetching commits and posting build statuses.
|
|
gitea:
|
|
baseUrl: https://git.example.org # base URL of the Gitea instance
|
|
owner: my-org # repository owner (user or organisation) for Gitea API (e.g. status checks)
|
|
repo: my-repo # repository name
|
|
statusContext: werkator # label shown on Gitea commit status checks (default: werkator)
|
|
|
|
# Build execution settings, enforced for all builds regardless of their trigger
|
|
# (watcher, UI restart, CLI build/retry).
|
|
executor:
|
|
# How many builds may run at the same time.
|
|
# At most one build per branch runs regardless; each branch builds in its own
|
|
# git worktree under .git/werkator/worktrees/, never in the primary checkout.
|
|
# Changing this value requires a restart.
|
|
maxConcurrent: 1
|
|
|
|
# Named build definitions (jobs, see notes below); every key names a build.
|
|
builds:
|
|
# "default" is the base every other definition inherits its settings from — never its
|
|
# trigger — and, with a trigger of its own, the build of every branch it selects.
|
|
# Without this entry an implicit default build (onPush over all branches) applies;
|
|
# writing it replaces that implicit one, so a default without a trigger is a settings
|
|
# base and nothing else.
|
|
default:
|
|
# When this build runs and for which branches — the only part a definition does NOT
|
|
# inherit from builds.default; everything below the block does.
|
|
trigger:
|
|
onPush: true # build every new commit of the selected branches
|
|
# branches: ["*", "!master"] # names or globs; a "!" pattern excludes; default: all
|
|
# atTimes: ["01:00"] # daily UTC times HH:MM ("??:05" = every hour at :05)
|
|
# activeWithin: 24h # only branches with commits in the last 24h
|
|
# afterSuccessOf: test # run after every green run of that build, at its commit (pinned)
|
|
# run before each build
|
|
cleanCommand: rm -rf build
|
|
# shell command for each build
|
|
buildCommand: ./gradlew --console=plain --no-daemon test
|
|
# directories copied as build artifacts; each is archived at its own
|
|
# workspace-relative path, except build/reports, which archives as reports/
|
|
# and is browsed by the artifact page's report index
|
|
artifactDirs:
|
|
- build/reports
|
|
- build/doc
|
|
stdoutLog: build.stdout.log # filename for captured stdout
|
|
stderrLog: build.stderr.log # filename for captured stderr
|
|
# Build a selected branch only while its head commit matches a pull-request head on
|
|
# origin (refs/pull/*/head — read via plain git, no API token needed; see notes below).
|
|
# Pinned: a branch cannot set this in its own committed config.
|
|
requirePullRequest: false
|
|
# Gitea check this build reports as; empty uses gitea.statusContext. Two builds of one
|
|
# commit under the same context overwrite each other. Pinned like requirePullRequest.
|
|
statusContext: ""
|
|
# Optional Docker build runtime; when enabled, the clean and build commands
|
|
# run inside a container instead of natively (see notes below).
|
|
docker:
|
|
# run clean/build commands in a Docker container (pinned)
|
|
enabled: false
|
|
# image for the build container; required when enabled
|
|
image: ""
|
|
# Dockerfile to (re)build the image from when it is missing or stale; empty pulls the image as-is
|
|
dockerfile: ""
|
|
# Docker build context used with dockerfile
|
|
context: "."
|
|
# Docker network mode for the build container; empty = Docker default (pinned)
|
|
network: ""
|
|
# additional environment variables set inside the build container
|
|
env: {}
|
|
|
|
# Every further job inherits the settings above and brings its own trigger.
|
|
# The nightly rebuild runs the full check instead of the quick on-commit one and is
|
|
# recorded separately as master@pitest:
|
|
pitest:
|
|
trigger:
|
|
atTimes: ["01:00"]
|
|
branches: ["master", "release/*"]
|
|
activeWithin: 24h
|
|
buildCommand: ./gradlew -PfullPitTest --console=plain --no-daemon piTestFull
|
|
artifactDirs: [build/reports, build/libs]
|
|
statusContext: werkator/pitest
|
|
|
|
# Build artifact storage and retention.
|
|
artifacts:
|
|
# Root directory for stored build artifacts.
|
|
# Empty means the platform default: $XDG_STATE_HOME (or ~/.local/state) plus /werkator/artifacts/<repo-key>,
|
|
# where <repo-key> is the sanitized absolute repository path.
|
|
# A leading ~/ expands to the home directory; a relative path is resolved against the repository.
|
|
rootDir: ""
|
|
# number of builds to keep per branch
|
|
retentionPerBranch: 3
|
|
# Additionally drop builds older than this age; suffixes s (seconds), m (minutes), h (hours), d (days).
|
|
# Empty means no age limit.
|
|
# Combines with retentionPerBranch: a build is kept only while it satisfies both limits.
|
|
# A branch's newest build is never age-pruned, so dormant branches keep their last status.
|
|
retentionMaxAge: ""
|
|
# Keep each branch's latest green (successful) build even beyond retentionPerBranch and retentionMaxAge.
|
|
# This backs the permanent artifact URLs /branches/<branch-key>/... — they always serve
|
|
# the latest green build of a branch and stay valid while newer builds fail.
|
|
# The kept build is still dropped once its branch is deleted from origin.
|
|
keepLatestGreen: true
|
|
|
|
# Controls the branch-polling loop.
|
|
watcher:
|
|
# delay between poll cycles; suffixes s (seconds), m (minutes), h (hours), d (days)
|
|
pollInterval: 10s
|
|
# max commit age for new origin branches to be pulled automatically
|
|
newBranchMaxAge: 5d
|
|
# Honor the builds.<name>.requirePullRequest gates (see notes below).
|
|
# Set false for a plain git origin without pull-request refs (no Gitea/GitHub);
|
|
# gated branches then build on new commits like any other branch.
|
|
pullRequestGate: true
|
|
# At the end of each poll cycle, fast-forward the primary checkout's local branch refs
|
|
# to their origin counterparts (see notes below). Fast-forward only — a diverged or
|
|
# ahead local branch is never touched. Set false to leave refs/heads/* alone entirely.
|
|
fastForwardLocalRefs: true
|
|
|
|
```
|
|
|
|
### Notes on `server.bindAddress`
|
|
|
|
The default is `127.0.0.1`.
|
|
Neither the web UI nor the JSON API authenticates read access — which is intended, so build states and artifacts can be linked from anywhere — so Werkator is meant to sit behind the host's reverse proxy rather than on a public interface.
|
|
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches Werkator over the Docker bridge, not over loopback), or when the proxy runs on another host.
|
|
Installations created before v0.9.9 have `bindAddress: 0.0.0.0` written into their `.werkator.yml` and keep it; the new default only applies where the key is absent or `init` writes a fresh file.
|
|
|
|
### Notes on `server.nginx`
|
|
|
|
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves Werkator over HTTPS (ADR 0005).
|
|
This is meant for hosts that provide Docker but no usable reverse proxy (e.g. Hostsharing managed containers); otherwise prefer the reverse-proxy setup in [deployment.md](deployment.md).
|
|
Certificates are obtained and renewed via Let's Encrypt (certbot Docker container, webroot mode), so `serverName` must be a public DNS name pointing at the host and `httpPort` must be reachable from the internet as port 80 (or via a port forward).
|
|
When `server.publicBaseUrl` is empty and `serverName` is set, it defaults to `https://<serverName>/`.
|
|
All nginx/certificate failures are non-fatal warnings; the plain HTTP server keeps running without the proxy.
|
|
The container is labelled `org.hoennig.werkator`; stale nginx containers of the repository are removed before each start, and the container is removed on shutdown.
|
|
`server.port` must differ from `httpPort` and `httpsPort`.
|
|
|
|
### Notes on `builds.<name>.requirePullRequest`
|
|
|
|
The gate applies to all watcher-triggered builds (push-triggered and scheduled auto builds).
|
|
A manual `werkator build <branch>` always builds, regardless of this setting.
|
|
|
|
Detection works without a Gitea API token:
|
|
the watcher lists `refs/pull/*/head` on origin via `git ls-remote` and builds a branch only when its head commit equals one of those pull-request head commits.
|
|
This ls-remote call is made at most once per poll cycle, and only when a branch requiring a pull request is otherwise due.
|
|
|
|
Because matching is by commit id, a closed pull request whose head ref still equals the branch head also counts.
|
|
Distinguishing open from closed pull requests would require the Gitea API.
|
|
|
|
To build pull-request branches only, gate the default build and give the permanent branches a build of their own:
|
|
|
|
```yaml
|
|
builds:
|
|
default:
|
|
trigger:
|
|
onPush: true
|
|
branches: ["*", "!main"]
|
|
requirePullRequest: true
|
|
main:
|
|
trigger:
|
|
onPush: true
|
|
branches: ["main"]
|
|
requirePullRequest: false
|
|
```
|
|
|
|
Without that second definition, direct pushes and merges to `main` would never build — merge commits do not match any pull-request head.
|
|
The `!main` exclusion keeps the default build off it, so a push is built once instead of by both definitions.
|
|
|
|
A plain git origin (no Gitea/GitHub) serves no `refs/pull/*/head` at all, so gated branches would never build there.
|
|
For such origins, disable all gates globally with `watcher.pullRequestGate: false` — typically in the machine-specific `.git/werkator/.werkator.yml`, so the committed configuration keeps the gates for forge-backed environments.
|
|
|
|
### Notes on `builds` (build definitions)
|
|
|
|
Every key of the `builds` section names a build definition (a job) over the branches — ADR 0007.
|
|
A build definition has a `trigger` block — when it runs and for which branches — and the settings that say what it does.
|
|
The split is structural because `trigger` is the one part never inherited from `builds.default`.
|
|
Writing any of its keys outside the block is refused with a message naming the definition: ignoring them would leave the build without a trigger, and a job that silently stops running is worse than a configuration that refuses to load.
|
|
|
|
Triggers: `onPush: true` builds every new commit of the selected branches; `atTimes: ["HH:MM", …]` rebuilds their heads once per day and slot (UTC).
|
|
A slot may also be written as `??:MM` — that minute of every hour, expanded to its 24 slots, so the build runs hourly.
|
|
Only the latest due slot of a day triggers, so slots missed while the server was down are skipped instead of piling up, and a slot whose pool is still building is retried on the next poll cycle until it succeeds.
|
|
A definition may combine them; one with none of the three triggers (`onPush`, `atTimes`, `afterSuccessOf`) never runs automatically — which is how `builds.default` is written when it is meant as a settings base only.
|
|
Werkator logs a warning once when no definition has a trigger at all, because such an instance never builds anything on its own.
|
|
|
|
#### Follow-up builds
|
|
|
|
`afterSuccessOf: <name>` makes a definition a *follow-up* of another one, its *predecessor*: it runs on the predecessor's branch at the predecessor's commit whenever a run of the predecessor ends green.
|
|
This is how a deployment is configured: a deployment is a build that follows a green build, and it gets everything a build has — its own row in History with log and duration, its own Gitea check under its own `statusContext`, restart, cancel, artifacts, and the per-branch serialization.
|
|
|
|
Every green run counts, whatever started it — the push watcher, an `atTimes` slot, a UI restart, `werkator retry`, or the startup recovery — and a repeated green run of the same commit triggers the follow-up again.
|
|
The follow-up builds the commit that was tested, not the branch's current origin head.
|
|
A failed, cancelled, or interrupted run triggers nothing.
|
|
The follow-up runs in the branch's worktree, after its predecessor, in the branch's sandbox or container — so a deployment tool like the Docker CLI is provided the way a compiler is.
|
|
It inherits `builds.default` like every definition, so a deployment that wants the predecessor's output has to set `cleanCommand: ""` itself; a deployment command should rather be self-contained and rebuild what it ships, because a restart of the follow-up alone runs without its predecessor.
|
|
The pull-request gate is not consulted for a follow-up: the predecessor passed it for the same commit, and the follow-up's `branches` selector is its own gate.
|
|
|
|
The trigger of a follow-up is pinned: a branch's committed config can neither add `afterSuccessOf` to a definition nor change the `trigger` block of a definition the host defines as a follow-up, so a branch cannot deploy itself.
|
|
What the deployment *does* comes with the repository, like every build command; where and when it happens, and with which credentials, is the host's.
|
|
The host's part lives in `.git/werkator/.werkator.yml`; credentials reach the sandbox like any build setting, through `werkdock.env`/`docker.env`, and a file such as an SSH key through the werkdock sandbox's persistent toolchain home, `.git/werkator/buildenv/home/` on the host, which the sandbox mounts as `/root`.
|
|
|
|
```yaml
|
|
# .git/werkator/.werkator.yml — the host's part: when, for which branches, with what
|
|
builds:
|
|
deploy:
|
|
trigger:
|
|
afterSuccessOf: frontend
|
|
branches: ["main"]
|
|
statusContext: werkator/deploy
|
|
werkdock:
|
|
env:
|
|
DEPLOY_TARGET: user@host:~/doms/example.org/htdocs-ssl
|
|
```
|
|
|
|
```yaml
|
|
# .werkator.yml — the repository's part: what
|
|
builds:
|
|
deploy:
|
|
cleanCommand: ""
|
|
buildCommand: scripts/deploy-prod.sh -y "$DEPLOY_TARGET"
|
|
```
|
|
|
|
A follow-up whose predecessor no definition has, and a cycle of follow-ups, refuse the start with a message naming the definition — a deployment that silently never runs is the failure the flat-key refusal exists to prevent.
|
|
A branch whose committed config drops or renames the predecessor only loses its follow-up, with a warning naming the branch.
|
|
A one-shot `werkator build` runs no follow-ups — its process ends with its build — and says which ones the server would have run.
|
|
|
|
Selector: `trigger.branches` lists branch names or glob patterns (`*` matches any characters, also across `/`); empty selects all origin branches.
|
|
A pattern prefixed with `!` excludes instead, and an exclusion always wins regardless of order — `["*", "!master"]` is every branch but master.
|
|
That is how a branch gets a build of its own without being built by the default one as well.
|
|
`activeWithin` (e.g. `24h`) additionally keeps only branches whose origin head commit is younger than the duration — useful to run a nightly deep check over all recently active branches.
|
|
Both parts combine as an intersection.
|
|
|
|
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `werkdock` with all their keys.
|
|
A definition carries the complete description of its build; unset keys fall back to `builds.default` and then to Werkator's own defaults.
|
|
`requirePullRequest`, `statusContext`, `docker.enabled`, `docker.network`, `werkdock.enabled`, `werkdock.rootfs`, and `werkdock.binary` are pinned (master-pinned, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci)): they are read from the repo install/project config even when a branch sets them in its own committed config.
|
|
So is the trigger of a [follow-up build](#follow-up-builds).
|
|
Inheritance from `builds.default` covers the settings only — the `trigger` block says when and where *this* build runs and is never inherited.
|
|
Definitions are part of the branch layer: a branch may add its own and override those from the project config, for its own builds only.
|
|
Because the inheritance is applied after all layers are merged, a build a branch invents still inherits the host's `builds.default` — its sandbox policy included, which is what keeps the pinning effective for a build the host has never heard of.
|
|
|
|
The implicit `default` build (`onPush: true`, all branches) preserves the behavior without any definitions; defining other builds does not disable it, `builds.default.onPush: false` does.
|
|
The `default` build records under the plain branch name; every other build records under `<branch>@<name>` with its own row in the branches view (sorted after its branch), its own `retentionPerBranch` count, latest status, and permanent latest-green artifact link.
|
|
The URL key is the sanitized pool name — `master@pitest` is served as `/branches/master_pitest/…`.
|
|
The pools live as long as the underlying branch exists on origin.
|
|
Restart, `werkator retry`, and the startup recovery re-run a build under its recorded definition, resolving the settings from the current configuration — the job definition is the source of truth, not the historical run.
|
|
The builds still run in their branch's worktree, one build per branch at a time.
|
|
The Gitea commit status is reported per commit under `gitea.statusContext`, so two builds of the same commit overwrite each other's check — give the second one its own `statusContext` (`werkator/quick`, say), or keep them apart with an exclusion pattern.
|
|
|
|
The concurrency limit that used to live in this section moved to `executor.maxConcurrent` without an alias.
|
|
A leftover `builds.maxConcurrent` key (or any other scalar where a definition belongs) is ignored with a warning, not a startup failure — a committed config cannot always be changed right away.
|
|
|
|
### The legacy `branches` section
|
|
|
|
Before build definitions existed, the settings lived in a per-branch `branches` section, with `branches.default` as the fallback for every branch not listed.
|
|
That section is deprecated and will be removed.
|
|
It is still read, but only while the merged configuration defines no build at all: as soon as one real definition exists, `branches` is ignored completely and a warning names it.
|
|
`builds.maxConcurrent` is not a definition — a configuration carrying only that leftover still uses `branches`.
|
|
|
|
Either or, never both: a definition now carries the complete description of its build, and two half-answers would silently pull against each other.
|
|
The decision is made on the merged configuration, so a machine config that defines builds switches `branches` off for every branch, including the branches whose committed config still has one.
|
|
|
|
`branches.<name>.autoBuild` (`enabled` + `times`) is the pre-ADR-0007 schedule that goes with it: it rebuilds the branch's own pool with its regular command and logs a deprecation warning.
|
|
`autoBuild.times` entries carrying their own `buildCommand`/`name` (a short-lived v0.9.13 syntax) are no longer supported — use a build definition.
|
|
|
|
To migrate, move `branches.default` to `builds.default`, add `onPush: true`, and turn every other branch entry into a definition with a `branches` selector of its own.
|
|
|
|
### Notes on `watcher.fastForwardLocalRefs`
|
|
|
|
Builds run in worktrees that share the primary checkout's `.git`, so a build tool can read `refs/heads/*` there.
|
|
Werkator itself never needs those refs to be current — it builds the commit `refs/remotes/origin/<branch>` points at — but build tools do.
|
|
A common case is a check that refuses to run when the local main branch differs from its origin counterpart; without this key it would fail on every build once origin moved on, because nothing would ever advance the local ref.
|
|
|
|
The fast-forward runs at the end of the poll cycle, after the due branches were enqueued.
|
|
That order is required, not cosmetic: a local ref lagging behind origin is exactly how the watcher recognizes new commits, so a ref kept in sync earlier — by this key, a cron job, or a mirroring fetch refspec (`+refs/heads/*:refs/heads/*`) — would silently stop the branch from ever being built.
|
|
|
|
Only fast-forwards are applied, as a compare-and-swap against the commit just read.
|
|
A local branch that diverged from origin or is ahead of it stays untouched, so local work in the primary checkout is never lost.
|
|
The branch checked out in the primary checkout is advanced with `git merge --ff-only`, which refuses to overwrite conflicting uncommitted changes; a refusal is logged and the cycle continues.
|
|
|
|
### Notes on `builds.<name>.docker`
|
|
|
|
With `docker.enabled`, Werkator shells out to the `docker` CLI; the `docker` command must be on the `PATH`.
|
|
When `dockerfile` is set, the image is (re)built whenever the Dockerfile content, its path, or the context path changed.
|
|
Staleness is tracked via the image label `org.werkator.build-inputs-sha256`.
|
|
A Gradle cache volume `werkator-gradle-<repo-key>` is created per repository and mounted as `GRADLE_USER_HOME`.
|
|
The build worktree is bind-mounted into the container; after each command the ownership of `build/` and `.gradle/` is repaired to the host user.
|
|
Git works inside the container: the primary repository's `.git` is mounted read-only (so build steps can run read-only git commands like `git log` or `git describe`), with `.git/werkator/` masked by an empty tmpfs so the build can never read the machine config (`git.token`) or the control token.
|
|
Note that the rest of `.git` — including `.git/config` — is visible to builds; Werkator never stores credentials there, and neither should you.
|
|
The Docker socket is mounted into the container and `DOCKER_HOST`/`TESTCONTAINERS_*` variables are set, so Testcontainers-based builds work inside the container.
|
|
All Werkator containers carry `org.hoennig.werkator` labels; stale build containers of the repository are removed before the first Docker build after a restart.
|
|
|
|
### Notes on `builds.<name>.werkdock`
|
|
|
|
With `werkdock.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
|
|
This is the third runtime, for hosts without root and without a Docker daemon (e.g. Hostsharing managed webspaces); see `docs/plan/17-bwrap-build-runtime.md` and ADR 0008.
|
|
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`werkdock.binary`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
|
|
The section was called `bwrap` and its binary key `bwrap.werkdock` until v1.2.0; both are still read, with a warning naming the file, so an installation can be migrated at its next configuration edit rather than at the next update.
|
|
`werkdock doctor` checks the host's capability (it replaced the retired `tools/werkator-build-prerequisites.sh` in step 23).
|
|
|
|
`werkdock.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
|
|
It is a local path or an `http(s)` URL; a URL is downloaded once into `.git/werkator/buildenv/`.
|
|
Build the archive with `tools/build-bwrap-rootfs.sh` on any machine with Docker.
|
|
The archive is loaded once per source as the werkdock image `werkator-buildenv-<hash>` into werkdock's store (`$WERKDOCK_HOME`, default `~/.werkdock`) — shared by every repository of this OS user; the hash derives from the source string, so a changed `rootfs` loads a fresh image and stale ones can be removed from the store.
|
|
Per-repo Gradle caches persist in `.git/werkator/buildenv/home`, bound as `/root`.
|
|
`werkdock.env` adds environment variables inside the sandbox; the environment is otherwise cleared (docker semantics) — the server's environment does not leak in.
|
|
Files created inside the sandbox are owned by the host user, because uid 0 maps back to the unprivileged webspace user.
|
|
|
|
`docker` and `werkdock` are mutually exclusive per branch: enabling both is rejected at start, not silently picked.
|
|
Git works inside the sandbox exactly as inside the Docker container: the primary `.git` is mounted read-only with `.git/werkator/` masked, so builds can run read-only git commands but never reach the machine config or the control token.
|
|
|
|
## `.git/werkator/.werkator.yml` (not committed)
|
|
|
|
```yaml
|
|
# Machine- or user-specific overrides and secrets. Keys here win over .werkator.yml.
|
|
git:
|
|
account: my-user # technical username for git HTTPS authentication
|
|
token: glpat-xxxxxxxxxxxxxxxxxxxx # Gitea API token — never commit this
|
|
```
|