A build definition carries the whole build; branches is legacy

`builds` and the legacy `branches` are now either/or: `branches` is read
only while the merged configuration defines no build at all — a leftover
`builds.maxConcurrent` is not one — and ignored with a warning as soon as
one exists. Two half-answers to "what does this build run" would silently
pull against each other, and the committed configs still carrying both
must not change behaviour before they are migrated.

A definition therefore gained the settings it was missing:
`requirePullRequest` and `docker.enabled`/`network`. Those stay pinned —
`stripPinned` now removes them from a branch layer wherever they appear,
in a definition as well as in a legacy branch entry.

`builds.default` becomes the base every other definition inherits its
settings from, never its trigger: `onPush`, `atTimes`, `branches`, and
`activeWithin` say when and where *this* build runs. The inheritance is
applied after all layers are merged, which is what makes a build invented
on a branch inherit the host's sandbox policy instead of the data-class
default — otherwise a branch could get a native build past the pinning by
defining a job the host has never heard of.

Two bugs found on the way, both the same shape as the build command the
artifact page used to get wrong:

- `FileArtifactStore` read the artifact directories from the plain branch
  settings, so a job adding its own `artifactDirs` never had them stored.
  It goes through `GitTallyConfig.buildSettings` now, like everything else
  that asks what a build runs.
- `Watcher.definitionsFor` cached the per-branch definitions by head
  commit alone, so an edited machine or project config only took effect
  once the branch moved — on a quiet branch, never. The primary config is
  part of the cache key now.
This commit is contained in:
mhoennig
2026-08-29 11:18:35 +02:00
parent f07a399f2e
commit 729eea5e6c
12 changed files with 427 additions and 214 deletions
@@ -175,17 +175,40 @@ class InitCommand(
# how many builds may run at the same time (at most one build per branch regardless)
maxConcurrent: 1
# Named build definitions (jobs) over the branches; every key names a build.
# A branch may add or override definitions in its own committed .gittally.yml —
# they then apply to that branch alone, so a new job can be tried out on a branch.
# Named build definitions (jobs); every key names a build.
# "default" is the base every other definition inherits its settings from — never
# its trigger — and is itself the build of every branch as long as it has one.
# A branch may add or override definitions in its own committed .gittally.yml;
# they apply to that branch alone, so a new job can be tried out on one branch.
builds:
# Example definition — triggers (onPush/atTimes), branch selector
# (branches/activeWithin), and overrides of the branch settings:
default:
onPush: true # build every new commit of the selected branches
# 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
artifactDirs:
- build/reports
stdoutLog: build.stdout.log # filename for captured stdout
stderrLog: build.stderr.log # filename for captured stderr
# build only while the branch head matches a pull-request head on origin
# (refs/pull/*/head — read via plain git, no API token needed);
# pinned: a branch cannot set this in its own committed config
requirePullRequest: false
docker:
enabled: false # run clean/build in a container instead of natively (pinned)
image: "" # image for the build container; required when enabled
dockerfile: "" # Dockerfile to (re)build the image from when missing or stale; empty pulls the image as-is
context: "." # Docker build context used with dockerfile
network: "" # Docker network mode for the build container; empty = Docker default (pinned)
env: {} # additional environment variables set inside the build container
# Further jobs inherit those settings and add their own trigger and selector:
# pitest:
# atTimes: ["01:00"] # daily UTC times HH:MM ("??:05" = every hour at :05)
# branches: ["master"] # names or glob patterns; default: all branches
# activeWithin: 24h # only branches with recent commits
# buildCommand: ./gradlew piTestFull
# buildCommand: ./gradlew pitestFull
# Build artifact storage and retention.
artifacts:
@@ -206,40 +229,12 @@ class InitCommand(
pollInterval: 10s
# max commit age for new origin branches to be pulled automatically
newBranchMaxAge: 5d
# honor branches.<name>.requirePullRequest; set false for a plain git origin
# honor builds.<name>.requirePullRequest; set false for a plain git origin
# without pull-request refs (refs/pull/*/head) — gated branches then build on new commits
pullRequestGate: true
# after enqueueing, fast-forward the primary checkout's local branch refs to origin,
# so build tools reading the shared .git see the same refs (diverged branches stay untouched)
fastForwardLocalRefs: true
# Per-branch build configuration.
# Use "default" as the fallback for all branches not listed explicitly.
branches:
default:
# 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
artifactDirs:
- build/reports
stdoutLog: build.stdout.log # filename for captured stdout
stderrLog: build.stderr.log # filename for captured stderr
# build only while the branch head matches a pull-request head on origin
# (refs/pull/*/head — read via plain git, no API token needed)
requirePullRequest: false
# DEPRECATED: define a build with atTimes in the builds section instead
autoBuild:
enabled: false # whether to rebuild on schedule
times: ["01:00"] # UTC times HH:MM for scheduled builds
docker:
enabled: false # run clean/build commands in a Docker container instead of natively
image: "" # image for the build container; required when enabled
dockerfile: "" # Dockerfile to (re)build the image from when missing or stale; empty pulls the image as-is
context: "." # Docker build context used with dockerfile
network: "" # Docker network mode for the build container; empty = Docker default
env: {} # additional environment variables set inside the build container
""".trimIndent()
file.toFile().writeText(content + "\n")
println("created ${file.toFile().relativeTo(normalizedWorkingDir.toFile())}")