A build definition says when it runs in a trigger block of its own
`onPush`, `atTimes`, `branches`, and `activeWithin` move into a nested `trigger`. The split is structural on purpose: the inheritance from `builds.default` now subtracts one key instead of a list of four, so a selector added to `TriggerConfig` later is non-inheritable by construction rather than because someone remembered to extend the list. A definition still writing those keys flat is refused by name, per file and scoped like the version check — the machine and project config abort the start, a branch's committed config fails only that branch. Ignoring them would leave the build with no trigger at all, which is a job that quietly stops running: the failure this refusal exists to prevent. Two more things a definition can now say: - A `!` prefix in `trigger.branches` excludes, and an exclusion wins whatever the order. `["*", "!master"]` gives one branch a build of its own without the default build running over it as well — until now the only way out of that double build was to drop the second definition's push trigger. - `statusContext` overrides the Gitea check this build reports as, empty keeping the repository-wide one. Two builds of a commit shared a context and overwrote each other's result, so a quick check beside a long build was not readable in Gitea. Pinned like `requirePullRequest`: a branch that could pick its context could take over the check a branch protection rule depends on. Fixed on the way: a branch whose builds all belong to named definitions rendered an empty row in the branches view, reading as "never built" directly beside its real builds. That row was unreachable before the exclusion patterns made such a branch possible.
This commit is contained in:
+38
-19
@@ -162,11 +162,18 @@ executor:
|
||||
# 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 onPush, the build of every branch. 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.
|
||||
# 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:
|
||||
onPush: true # trigger: build every new commit of the selected branches
|
||||
# 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
|
||||
# run before each build
|
||||
cleanCommand: rm -rf build
|
||||
# shell command for each build
|
||||
@@ -181,6 +188,9 @@ builds:
|
||||
# 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:
|
||||
@@ -197,16 +207,17 @@ builds:
|
||||
# additional environment variables set inside the build container
|
||||
env: {}
|
||||
|
||||
# Every further job inherits the settings above and adds its own trigger and selector.
|
||||
# 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:
|
||||
onPush: false # trigger: build every new commit (default: false)
|
||||
atTimes: ["01:00"] # trigger: daily UTC times HH:MM, "??:05" = hourly at :05
|
||||
branches: ["master", "release/*"] # selector: names or glob patterns (default: all)
|
||||
activeWithin: 24h # selector: only branches with commits in the last 24h
|
||||
trigger:
|
||||
atTimes: ["01:00"]
|
||||
branches: ["master", "release/*"]
|
||||
activeWithin: 24h
|
||||
buildCommand: ./gradlew -PfullPitTest --console=plain --no-daemon piTestFull
|
||||
artifactDirs: [build/reports, build/libs]
|
||||
statusContext: GitTally/pitest
|
||||
|
||||
# Build artifact storage and retention.
|
||||
artifacts:
|
||||
@@ -279,16 +290,19 @@ To build pull-request branches only, gate the default build and give the permane
|
||||
```yaml
|
||||
builds:
|
||||
default:
|
||||
onPush: true
|
||||
trigger:
|
||||
onPush: true
|
||||
branches: ["*", "!main"]
|
||||
requirePullRequest: true
|
||||
main:
|
||||
onPush: true
|
||||
branches: ["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.
|
||||
Note that `main` is then selected by both definitions, so a push builds it twice; give the default build a `branches` selector that excludes it, or accept the second run.
|
||||
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/gittally/.gittally.yml`, so the committed configuration keeps the gates for forge-backed environments.
|
||||
@@ -296,7 +310,9 @@ For such origins, disable all gates globally with `watcher.pullRequestGate: fals
|
||||
### 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 triggers, a branch selector, and build-setting overrides.
|
||||
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.
|
||||
@@ -304,14 +320,16 @@ Only the latest due slot of a day triggers, so slots missed while the server was
|
||||
A definition may have both; one with neither never triggers automatically — which is how `builds.default` is written when it is meant as a settings base only.
|
||||
GitTally logs a warning once when no definition has a trigger at all, because such an instance never builds anything on its own.
|
||||
|
||||
Selector: `branches` lists branch names or glob patterns (`*` matches any characters, also across `/`); empty selects all origin branches.
|
||||
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`, and `docker` with all its keys.
|
||||
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` with all its keys.
|
||||
A definition carries the complete description of its build; unset keys fall back to `builds.default` and then to GitTally's own defaults.
|
||||
`requirePullRequest`, `docker.enabled`, and `docker.network` are pinned: they are read from the repo install/project config even when a branch sets them in its own committed config, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci).
|
||||
Inheritance from `builds.default` covers the settings only — a trigger and a selector say when and where *this* build runs, so `onPush`, `atTimes`, `branches`, and `activeWithin` are never inherited.
|
||||
`requirePullRequest`, `statusContext`, `docker.enabled`, and `docker.network` are pinned: they are read from the repo install/project config even when a branch sets them in its own committed config, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci).
|
||||
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.
|
||||
|
||||
@@ -320,7 +338,8 @@ The `default` build records under the plain branch name; every other build recor
|
||||
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, `gittally 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, and the Gitea commit status is reported per commit in the shared status context (the last build of a commit wins there).
|
||||
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` (`GitTally/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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Step 18: Remove the legacy `branches` section, group the trigger
|
||||
# Step 18: Remove the legacy `branches` section
|
||||
|
||||
Prerequisites: none in code — v0.9.19 already made `builds` and `branches` either-or.
|
||||
Read `README.md` first.
|
||||
@@ -7,8 +7,7 @@ Scheduled for roughly **2026-09-05**, one week after v0.9.19 (2026-08-29), and o
|
||||
Build definitions describe a build completely since v0.9.19, and `branches` is read only while a configuration defines no build at all.
|
||||
This step deletes the section, its deprecated `autoBuild` schedule, and the either-or branch in the loader, and turns a leftover `branches:` key into a named error instead of a silent loss of settings.
|
||||
|
||||
It carries a second, unrelated break in the same release, deliberately: the trigger and selector keys of a definition move into a `trigger` block.
|
||||
Both changes force the same repositories to migrate the same files, so they cost one migration, one `FORMAT_BROKE_IN`, and one deploy together — and two of each apart.
|
||||
The `trigger` block, originally planned here, shipped earlier — see the section below for what that leaves.
|
||||
|
||||
## Precondition Check (run first, do not skip)
|
||||
|
||||
@@ -22,8 +21,6 @@ ssh tallyman@vm4006.hostsharing.net 'cd ~/hs.hsadmin.ng && grep -n "^branches:"
|
||||
|
||||
Expected output: nothing at all.
|
||||
|
||||
The same holds for the second change: no configuration may still carry a definition with a flat `onPush`, `atTimes`, `branches`, or `activeWithin`. Since the two migrations touch the same files, do them in one commit per repository.
|
||||
|
||||
As of 2026-08-29 this listed `master` and five `mihoe/…` branches, plus the machine config.
|
||||
The plan was: merge `mihoe/reactivate-pi-test` (the first branch with the new shape) to master, rebase the other branches onto the new master, then run this step.
|
||||
Branches without a committed `.gittally.yml` are fine — they build from the machine config.
|
||||
@@ -42,31 +39,14 @@ Note that `branches:` also exists as the *selector* key **inside** a build defin
|
||||
- `watcher/Watcher.kt`: delete `enqueueDeprecatedAutoBuilds`, its call in `enqueueDueBranches`, and the `warnedDeprecatedAutoBuild` flag.
|
||||
- `config/ConfigVersion.kt`: set `FORMAT_BROKE_IN` to this release's version and `FORMAT_BROKE_DESCRIPTION` to something like "the per-branch `branches` section was replaced by build definitions".
|
||||
This is the first real use of that mechanism: a file declaring `gitTally.version.since` below this release is then refused with a message naming the change.
|
||||
- `commands/InitCommand.kt`: the generated template is `builds`-only since v0.9.19, but its `default` entry and the commented example job need the `trigger` block. Verify with `gittally init` in a scratch repo.
|
||||
Note that it only bites files that declare a version — which is why the rejection by name above exists next to it, not instead of it.
|
||||
- `commands/InitCommand.kt`: nothing — the generated template has been `builds`-only with a `trigger` block since v0.9.20. Verify with `gittally init` in a scratch repo.
|
||||
|
||||
## The `trigger` block
|
||||
## Already done: the `trigger` block
|
||||
|
||||
`onPush`, `atTimes`, `branches`, and `activeWithin` move from the definition into a nested `trigger`:
|
||||
|
||||
```yaml
|
||||
builds:
|
||||
default:
|
||||
trigger:
|
||||
onPush: true
|
||||
branches: ["*", "!master"]
|
||||
buildCommand: ./gradlew check
|
||||
```
|
||||
|
||||
The point is that the inheritance rule becomes structural instead of a remembered list: everything except `trigger` is inherited from `builds.default`.
|
||||
Today `ConfigLoader.SELECTOR_KEYS` enumerates the four keys, and whoever adds a fifth selector without touching that list makes it silently inheritable — a job firing on branches that are none of its business, noticed only much later.
|
||||
After the change `mergeBuildDefaults` subtracts the single key `trigger`, and a new selector is automatically right.
|
||||
|
||||
- `config/BuildDefinition.kt`: a nested `TriggerConfig(onPush, atTimes, branches, activeWithin)`; `selects`/`selectsByName`/`maxAge` read from it.
|
||||
- `config/ConfigLoader.kt`: `SELECTOR_KEYS` becomes the single key `trigger`.
|
||||
- Reject a definition that still carries any of the four keys flat, by name and with the same scoping as the `branches` rejection. `FORMAT_BROKE_IN` does not cover this: the hs.hsadmin.ng configs declare no version, and a flat `onPush` nobody reads any more means the branch stops building, wordlessly.
|
||||
- `branches` inside `trigger` is the selector and unrelated to the removed top-level section — the two named the same thing, which is part of why the block is clearer.
|
||||
|
||||
`trigger` also groups `branches`/`activeWithin`, which are selectors rather than triggers in the strict sense. That is the established shape (GitHub Actions writes `on: push: branches: [...]`) and was chosen over `when`.
|
||||
`onPush`, `atTimes`, `branches`, and `activeWithin` moved into a nested `trigger` block in the release that made a definition self-contained, and writing them flat is refused since then.
|
||||
Nothing is left to do for it here beyond not reintroducing the flat shape in examples.
|
||||
`ConfigLoader.TRIGGER_KEYS` is already the single key the inheritance subtracts, so the removal below does not touch it.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -85,8 +65,7 @@ Add a test that a build whose definition was removed from the config still resol
|
||||
- `docs/configuration.md`: delete the section "The legacy `branches` section"; drop the "only while nothing defines a build" qualifier from the branch-layer section.
|
||||
- `AGENTS.md`: the invariant bullet starting "`builds` or the legacy `branches`, never both" becomes the rejection rule.
|
||||
- `.claude/skills/architecture/SKILL.md`: `resolveBuildSections` no longer chooses between two sections.
|
||||
- `docs/migration-from-legacy.md`: already maps to `builds.<name>`; re-check it reads correctly without the legacy section existing, and move the trigger keys of the mapping table into `trigger`.
|
||||
- Every YAML example in `docs/configuration.md`, `docs/deployment.md`, and the ADRs that shows a build definition needs the `trigger` block.
|
||||
- `docs/migration-from-legacy.md`: already maps to `builds.<name>`; re-check it reads correctly without the legacy section existing.
|
||||
|
||||
## Production
|
||||
|
||||
@@ -105,7 +84,7 @@ Deploy as usual (`docs/plan/15-runtime-bundle-distribution.md`), only while `/ap
|
||||
|
||||
- `gittally config:print --full` on vm4006 before the restart: no `branches` in the output, `builds.default` and `builds.master` complete, `master` still inheriting `docker.enabled: true` and `network: host`.
|
||||
- After the restart: no warnings about a branches section, the watcher polls without errors, and a branch build starts in `hsadmin-ng-build-env:latest`.
|
||||
- Deliberately: point the running instance at a scratch repository whose config still has `branches:`, and at one with a flat `onPush:`, and confirm both errors name the file and the way out.
|
||||
- Deliberately: point the running instance at a scratch repository whose config still has `branches:` and confirm the error names the file and the way out.
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ Added for the vm2176 → vm4006 migration (2026-08-10):
|
||||
|
||||
Added after v0.9.19 replaced the per-branch settings with build definitions (2026-08-29):
|
||||
|
||||
- [ ] `18-remove-branches-section.md` — delete the legacy `branches` section and its `autoBuild` schedule, and group a definition's trigger and selector keys in a `trigger` block; run around 2026-09-05, after the precondition check in the step file
|
||||
- [ ] `18-remove-branches-section.md` — delete the legacy `branches` section and its `autoBuild` schedule; run around 2026-09-05, after the precondition check in the step file
|
||||
|
||||
Added for running GitTally on Hostsharing Managed Webspaces (2026-08-10):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user