Files
werkator/docs/adrs/0007-2026-08-28.build-definitions.md
mhoennigandClaude Opus 5 35f06ec1ec 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>
2026-08-30 19:39:55 +02:00

126 lines
8.9 KiB
Markdown

# Build Definitions Replace Branch-Owned Schedules
**Status:**
- proposed: 2026-08-28
- accepted: 2026-08-28
- rejected: -
- superseded: -
**Decision [accepted]:** A top-level `builds` section defines named builds (jobs) with `onPush`/`atTimes` triggers and a branch selector — the branch-owned `autoBuild` schedule and the v0.9.13 per-slot `buildCommand`/`name` syntax are replaced by it.
`branches` stays what it is: per-branch build settings that every build inherits.
## Context and Problem Statement
Werkator's configuration is branch-centric: `branches.<name>` holds the build settings, and the nightly schedule (`autoBuild`) hangs off the branch.
v0.9.13 added a per-slot `buildCommand` and `name` to `autoBuild.times[]`, so a nightly slot could run a fuller check recorded in its own result pool.
That worked, but it is a job concept hidden inside a schedule entry: the slot carries a command, an identity, and (implicitly) a branch — everything a job has, in the wrong place.
Requirements that the branch-centric schema cannot express cleanly:
- Several builds of the same branch with different commands (quick check on push, full PIT test nightly).
- One scheduled build over a *set* of branches, e.g. "run `piTestFull` nightly for every branch that had commits in the last 24 hours" — a schedule owned by a branch cannot select branches.
- A name for such a build that is not welded to a single schedule slot.
### Technical Background
Since v0.9.13 the result model already carries a pool identity per build (`BuildResult.name`), with name-keyed history, retention, branches-view rows, and permanent latest-green links, while everything git-side (origin lookups, pruning, worktrees, Gitea) stays keyed by the real branch.
So the persistence and UI side is prepared; what is missing is a first-class job in the configuration and the watcher.
## Considered Options
* Keep extending the branch-owned `autoBuild` (status quo of v0.9.13)
* Top-level `builds` section with named build definitions (jobs)
* Both concepts permanently in parallel
### Keep Extending `autoBuild`
Every new need (per-slot command, per-slot name, branch selectors) grows another attribute inside `autoBuild.times[]`.
#### Advantages
- No schema change, no migration.
#### Disadvantages
- The slot *is* a job in disguise; each extension makes the disguise worse.
- A schedule owned by one branch can never select other branches (`activeWithin` is inexpressible).
- Two `buildCommand` keys at different nesting levels with override semantics (the complaint that triggered v0.9.13's rework already).
### Top-Level `builds` Section (chosen)
```yaml
branches: # unchanged: per-branch build settings, worktree-overridable
default:
cleanCommand: rm -rf build
buildCommand: ./gradlew --console=plain --no-daemon prQuickCheck
builds:
# implicit when omitted or not overridden — exactly the pre-builds behavior:
# default:
# onPush: true
pitest:
atTimes: ["01:00"] # UTC HH:MM, plain strings only
activeWithin: 24h # only branches with commits in the last 24h
# branches: ["master"] # alternative/additional explicit selector
buildCommand: ./gradlew -PfullPitTest --console=plain --no-daemon piTestFull
```
A build definition has:
- **Triggers**: `onPush: true|false` (default `false`; the implicit `default` build has `true`) and `atTimes: [HH:MM, …]` (default empty). A build may have both.
- **Branch selector**: `branches: [names]` (default: all origin branches) and `activeWithin: <duration>` (default: unset = no age filter; a branch qualifies while its origin head commit is younger than the duration). Both combine as an intersection.
- **Build-setting overrides**: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, and the docker image keys (`image`, `dockerfile`, `context`, `env`) — anything unset falls back to the merged branch settings.
Semantics:
- **Merge order** for the effective settings of one build on one branch: config defaults → `branches.default``branches.<branch>` → the worktree's committed `.werkator.yml` (build keys, pinned keys stripped) → `builds.<name>` overrides. The build definition wins last because it is the job; it comes from the repo install/project config (server-side), never from the worktree.
- **Pool identity**: the `default` build records under the branch name (URLs, rows, retention as before); every other build records under `<branch>@<build>` (URL-sanitized, e.g. `/branches/master_pitest/…`). Each pool keeps its own retention count, latest status, and permanent latest-green link.
- **Persistence**: the result stores the build's name (`build`, default `default`) next to the branch; the derived pool name keeps keying grouping and display. The v0.9.13 `buildCommandOverride` field is dropped: restart, retry, and startup recovery re-resolve the command from the *current* config by (branch, build) — a job definition in config is the source of truth, so a re-run of an old result uses the job's current command.
- **Triggers in the watcher**: `onPush` uses the existing change detection per pool ("already built" per pool and commit); `atTimes` fires once per day per slot per pool (state file keyed by pool, date, time). The `branches.<name>.requirePullRequest` gate stays a branch property and gates all watcher-triggered builds of that branch, as today.
- **Execution invariants unchanged**: every build of a branch runs in that branch's worktree, at most one build per branch at a time, `executor.maxConcurrent` across branches, Gitea commit status per commit in the shared status context (last build of a commit wins).
Compatibility and migration:
- No `builds` section, or no `default` entry: the implicit `default` build (`onPush: true`, all branches) preserves today's behavior exactly. Defining other builds does not disable it; `builds.default.onPush: false` does.
- `branches.<name>.autoBuild` (`enabled` + plain `times`) keeps working for compatibility, internally mapped to a scheduled build of the branch's own pool, with a deprecation warning in the log; removal is not scheduled.
- The v0.9.13 per-slot `buildCommand` and `name` are **removed** (not deprecated): released one day ago, configured nowhere.
#### Advantages
- Jobs are first-class: name, command, schedule, and branch selection in one place, no nesting tricks.
- Expresses the previously impossible selector cases (`activeWithin`, explicit branch lists) naturally.
- Reuses the v0.9.13 pool plumbing (result `name`, retention, rows, permalinks) almost unchanged.
- The trigger keys read as English: `onPush`, `atTimes`.
#### Disadvantages
- The biggest config-schema change since the rewrite: watcher enqueue logic, config layering, docs, `init` templates, and tests are all touched.
- Two sections (`branches`, `builds`) must be explained: settings per branch vs. jobs over branches.
- Re-runs follow the current job definition instead of the recorded command — a deliberate semantic change against v0.9.13.
### Both Concepts Permanently
Keep `autoBuild` (including the v0.9.13 slot syntax) forever next to `builds`.
#### Advantages
- Nothing ever breaks.
#### Disadvantages
- Two ways to express the same job, forever; every future feature must be specified against both.
## Decision Outcome
Top-level `builds` with `onPush`/`atTimes`, as specified above.
`branches.*.autoBuild` stays as a deprecated, mapped alias; the v0.9.13 slot extras are reverted.
Follow-up (2026-08-28): mixing the execution key `maxConcurrent` into the `builds` section as a reserved key proved confusing — it is not a build definition.
The concurrency limit moved to `executor.maxConcurrent` (a new section for execution settings), without a compatibility alias, so the `builds` section holds build definitions only.
Follow-up (2026-08-29): pinning the whole `builds` section against the branch layer was wrong and is reverted.
A branch's committed `.werkator.yml` describes that branch's CI, and a new `builds` configuration can only be tried out by committing it on a branch — pinned, it was neither effective at build time nor visible to the watcher, so the job silently did not exist.
The branch layer now carries `builds` too: the watcher reads each origin branch's committed config (`git show`, cached by head commit) to decide which of *that branch's* builds are due, and a branch's definitions are evaluated for that branch alone, so they can never trigger builds of another branch.
The pinned set is reduced to what does not describe this branch's build: secrets (`git`), the host/repository sections (`server`, `gitea`, `executor`, `watcher`), the sandbox policy (`docker.enabled`/`docker.network`), and the trust gate (`requirePullRequest`).
Letting a branch set its own `buildCommand` through a definition grants no new power — `branches.*.buildCommand` always allowed exactly that — whereas the sandbox and the gate decide whether untrusted branch code runs on the host at all, and therefore stay server-side.