From 5051c7bb992f5ed31dc2406662fb775a5c190350 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Fri, 28 Aug 2026 19:52:39 +0200 Subject: [PATCH] Propose ADR 0007: build definitions replace branch-owned schedules Co-Authored-By: Claude Fable 5 --- .../adrs/0007-2026-08-28.build-definitions.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/adrs/0007-2026-08-28.build-definitions.md diff --git a/docs/adrs/0007-2026-08-28.build-definitions.md b/docs/adrs/0007-2026-08-28.build-definitions.md new file mode 100644 index 0000000..12f65a3 --- /dev/null +++ b/docs/adrs/0007-2026-08-28.build-definitions.md @@ -0,0 +1,116 @@ +# Build Definitions Replace Branch-Owned Schedules + +**Status:** +- proposed: 2026-08-28 +- accepted: - +- rejected: - +- superseded: - + +**Decision [proposed]:** 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 + +GitTally's configuration is branch-centric: `branches.` 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: ` (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.` → the worktree's committed `.gittally.yml` (build keys, pinned keys stripped) → `builds.` 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 `@` (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..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, `builds.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..autoBuild` (`enabled` + plain `times`) keeps working for a transition period, internally mapped to a scheduled build of the branch's own pool, with a deprecation warning in the log; `docs/configuration.md` documents only `builds`. +- 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` survives one deprecation period as a mapped alias; the v0.9.13 slot extras are reverted.