Commit Graph
187 Commits
Author SHA1 Message Date
Michael HönnigandClaude Opus 5 d0169e57bc Renaming gitTally to Werkator because there is another git-related tool named gittally (#1)
* renaming from gitTally to Werkator

* 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>

* Move the pre-rename state directory at the first start

The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.

Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.

The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.

Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Document PR#1: the rename to Werkator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the legacy env-to-YAML conversion from the setup tool

The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.

The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.

It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.

`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Describe this repository's build with a build definition

Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.

Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop documenting the pre-rename fallback for users

Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.

Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Bring the PR-doc to its final state

The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.

Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Release v1.0.0: Werkator

The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct the PR-doc about the version

It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Point the legacy references at the history

`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.

The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Retarget the links in the historic PR-docs

The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add the v1.0.0 deployment procedure for vm4006

Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.

Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Rename the machine configuration along with its directory

Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.

`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Let init see a configuration under its previous name

`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.

Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Record the v1.0.0 deployment to vm4006

Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the control token left under the old localStorage key

The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.

Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:32:54 +02:00
mhoennigandClaude Opus 5 4d76744721 Record the v0.9.21 deployment to vm4006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 11:20:58 +02:00
mhoennigandClaude Opus 5 0229081570 Release v0.9.21: the watcher's reach into origin is visible
Also fixes the log-capture test added with step 19: the test JVM logs at
WARN, so the INFO line reporting the recovered fetch never reached the
appender. The helper now lowers the level explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 11:19:42 +02:00
mhoennigandClaude Opus 5 e4e4cd1899 Record step 19 as executed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 11:13:31 +02:00
mhoennigandClaude Opus 5 0193386642 Show an unreachable origin in the web UI (step 19)
A watcher that cannot fetch leaves the server perfectly reachable and
every branch row stale — a calm list that implies nothing changed. The
state was already recorded and served at /api/watcher; only nothing
rendered it.

The banner lives in the shared nav fragment, so every view inherits it,
and is fed from the view's own polling tick without being chained to it.
It stays separate from live-indicator: that one reports whether the
browser reaches the server, this one whether the server reaches origin.

Also caps the log volume the outage exposed: a lasting fetch failure is
logged when its message changes, not on every cycle, and the recovery is
logged once. Same for an invalid atTimes slot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 11:11:45 +02:00
mhoennigandClaude Opus 5 18a41e9ced Plan showing an unreachable origin in the web UI (step 19)
A wrong git token made the watcher fail every fetch for 57 minutes while
the branches view kept showing its last known list. WatcherState already
records lastFetchError and /api/watcher already serves it; only the UI
never renders it.

The step also folds in the logging volume: one outage wrote 297 identical
warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 09:03:42 +02:00
mhoennigandClaude Opus 5 ef8b46b6e1 Name the two kinds of pinned key: host-pinned and master-pinned
Pinning is one rule — strip the key from the branch layer — but the keys
fall into two groups by where they are meant to live: what only the
machine can know, and what belongs in the repository yet must not be
decided per branch. docker.enabled/network moves from the first group to
the second once the committed config carries it.

The KDoc says explicitly that the distinction is documentary, so nobody
looks for two mechanisms in stripPinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:26:47 +02:00
mhoennigandClaude Opus 5 93bed2bd45 The host loses its builds section once master carries one
Not a shrink to the pinned docker keys but a full removal: the pinning
strips the branch layer only, so master's committed config is merged
unstripped and its sandbox policy reaches every branch. The nightly job
moves there with it. A rollback below v0.9.19 is no longer provided for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 07:41:25 +02:00
mhoennigandClaude Opus 5 0e7f024de4 Record the host config cleanup that step 18 no longer has to do
The machine config on vm4006 lost its legacy branches block, renamed
builds.master to builds.nightly and moved build/libs into the default
definition's artifactDirs. Step 18 still described that work as pending
and named a rollback path to 0.9.18 that the file no longer supports.

What is left there is the shrinking to genuinely host-specific keys,
plus the open question where the nightly rebuild belongs once master
carries its own config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 07:26:50 +02:00
mhoennig 3aad887b8e Record the v0.9.20 deployment to vm4006 2026-08-29 12:09:22 +02:00
mhoennig f0f996a53c 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.
2026-08-29 12:05:10 +02:00
mhoennig 0fbb25b47f Plan the trigger block together with the branches removal 2026-08-29 11:47:56 +02:00
mhoennig 767470430b Plan the removal of the legacy branches section (step 18) 2026-08-29 11:26:42 +02:00
mhoennig abd40dd00f Record the v0.9.19 deployment to vm4006 2026-08-29 11:23:03 +02:00
mhoennigandClaude Opus 5 e25ebaa0bf Release v0.9.19
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:21:41 +02:00
mhoennig 729eea5e6c 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.
2026-08-29 11:18:35 +02:00
mhoennigandClaude Opus 5 f07a399f2e Record the v0.9.18 deployment to vm4006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 10:19:16 +02:00
mhoennigandClaude Opus 5 0428cf6737 Config files declare their GitTally version (v0.9.18)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 10:18:21 +02:00
mhoennigandClaude Opus 5 8608170f5b Configuration files declare the GitTally they are written for
Until now a version that renames or drops a key did not fail — it silently
ignored what it no longer understood, and the effect surfaced as a build
doing the wrong thing. Both directions of that happened within two days:
a branch config using `??:00` on a GitTally that did not know it yet, and a
`builds.maxConcurrent` that had moved to another section.

    gitTally:
      version:
        since: "0.9.18"   # enforced
        below: "2.0"      # release marker; GitTally decides how strictly

There is deliberately no version of the file format (no `apiVersion`): no API
is involved — GitTally reads its own configuration — and only one generation
is ever supported. The declaration exists to make an incompatibility
nameable, never to run two parsers.

`since` is hard in both directions. Too new a requirement is refused, and so
is a file written before the version in which the configuration format last
broke (`ConfigVersions.FORMAT_BROKE_IN`, empty for now) — that check needs no
declared ceiling, because GitTally knows its own breaking changes.

`below` is the team's release marker and only warns: an unmaintained caution
value must never stop a CI. The routine it serves is the one known from IDE
plugins — new version, warning, try it, then raise the marker and commit.

The reach of a violation follows the layer: the machine and project configs
abort the start naming the file and the rollback, while an incompatible
branch config fails only that branch's builds. A branch cut before a
migration must not stop the server or hold up the branches that are fine.
A file that declares nothing keeps working, and the CLI prints one line
instead of a stack trace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 10:15:35 +02:00
mhoennigandClaude Opus 5 fa183ef1db Record the v0.9.17 deployment to vm4006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:37:43 +02:00
mhoennigandClaude Opus 5 763af51501 Refresh on return from the background (v0.9.17)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:36:58 +02:00
mhoennigandClaude Opus 5 1d28ec2a3f Refresh the page when it comes back from the background
Durations of running builds are ticked client-side, so a page left in the
background kept counting up a build that had long finished on the server —
on a phone the tab is frozen for an hour and comes back showing a build
"running" for an hour.

Resuming now re-arms the poll unconditionally instead of only when the page
was stopped: a frozen page may never deliver the hidden event, and its
throttled interval then fires whenever the browser feels like it. `pageshow`
and `focus` are listened to as well, because not every browser reports a
returning page as a visibility change; resume events arriving together are
collapsed into one fetch.

Verified in a browser: hidden for 11s over a 10s interval makes no request,
a lone focus event makes exactly one, and a second resume event right after
makes none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:30:39 +02:00
mhoennigandClaude Opus 5 cd0596b17a Record the v0.9.16 deployment to vm4006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:12:01 +02:00
mhoennigandClaude Opus 5 dfbd67fa37 Hourly build slots and the true build command on the artifact page (v0.9.16)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:57:51 +02:00
mhoennigandClaude Opus 5 1a84b1fc1b Show the command a build actually runs on its artifact page
The artifact page read `branches.<branch>.buildCommand` from the primary
config only, so for a named build on a branch with its own config it showed
a command that build never ran — on vm4006 it showed the host config's
command for a build that ran the branch definition's `pitestFull`.

Resolving "what does this build run" now has one implementation,
`GitTallyConfig.buildSettings(branch, build)`: the branch entry with the
build definition's overrides applied last. The executor uses it, and the
page resolves it against the branch layer committed at the build's own
commit — the same inputs the executor had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:52:22 +02:00
mhoennigandClaude Opus 5 939d8eeb9c Hourly scheduled builds via a ??:MM slot
`atTimes: ["??:05"]` runs a build five past every hour. The pattern expands
to its 24 concrete slots before the due-slot match, so each hour is its own
slot in the trigger state and fires once — the existing per-slot semantics
carry over unchanged, including that only the latest due slot of a day
triggers and that a slot whose pool is still building is retried until it
starts.

Only the hour may be a wildcard; anything else is skipped with a warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:52:22 +02:00
mhoennigandClaude Opus 5 08a8a0bbb4 Record the v0.9.15 deployment to vm4006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:38:57 +02:00
mhoennigandClaude Opus 5 ca3e758cdc Ignore a leftover builds.maxConcurrent instead of refusing to start
The concurrency limit moved to executor.maxConcurrent without an alias, so
the old key binds a scalar where a build definition belongs and failed the
whole configuration. But that configuration is committed in the watched
repository, and a repository's master is not always changeable right away —
an installation must not be stuck on a key it is meant to forget.

A `builds` entry that is not a mapping is now dropped with a warning (once
per key, the config is loaded every poll cycle), naming executor.maxConcurrent
for the key that moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:37:10 +02:00
mhoennigandClaude Opus 5 f5871a0442 The branch config takes precedence, including its build definitions
A branch's committed .gittally.yml describes that branch's CI, so it wins
over the project and repo-install config — the `builds` section included.
Pinning it was wrong: a new build definition can only be tried out by
committing it on a branch, and pinned it neither took effect at build time
nor existed for the watcher, so the job silently never ran.

The watcher now decides per branch from that branch's own definitions,
reading its committed config via `git show` and caching it by head commit,
so the read happens only when the branch moved; an unreadable config falls
back to the primary definitions instead of failing the poll cycle. A
branch's definitions are evaluated for that branch alone, so a definition
committed on one branch can never trigger builds of another.

The pinned set is reduced to what does not describe this branch's build:
secrets (`git`), the host and repository sections (`server`, `gitea`,
`executor`, `watcher`), the sandbox policy (`docker.enabled`/`network`),
and the trust gate (`requirePullRequest`). Letting a branch set its own
build command through a definition grants no new power — `branches.*.
buildCommand` always allowed exactly that — while the sandbox and the gate
decide whether untrusted branch code runs on the host at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:29:26 +02:00
mhoennigandClaude Fable 5 a3faa172f8 Concurrency limit under executor.maxConcurrent (v0.9.15)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 21:24:51 +02:00
mhoennigandClaude Fable 5 495b7f3282 Move the concurrency limit to executor.maxConcurrent
builds.maxConcurrent mixed an execution setting into the build
definitions as a reserved key. The limit now lives in the new executor
section (pinned like the builds section, enforced for all builds
regardless of trigger), default 1, without a compatibility alias — a
leftover builds.maxConcurrent key is rejected as an invalid build
definition. Recorded as a follow-up in ADR 0007.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 21:04:25 +02:00
mhoennigandClaude Fable 5 e118c239f8 Record the v0.9.14 deployment to vm4006
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 20:50:13 +02:00
mhoennigandClaude Fable 5 3ff27dac0b Named build definitions (v0.9.14)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 20:47:28 +02:00
mhoennigandClaude Fable 5 0e8db18e69 Build definitions with onPush/atTimes replace branch-owned schedules
ADR 0007: the YAML builds section (next to the reserved maxConcurrent
key) defines named builds (jobs) with onPush/atTimes triggers, branch
selectors (name globs, activeWithin age filter), and build-setting
overrides applied last over the merged branch config. The implicit
default build (onPush over all branches) preserves the previous
behavior; the section is pinned against the worktree layer.

Results record the job name; restart, retry, and startup recovery
re-run by it, resolving settings from the current config. A non-default
build records under the <branch>@<build> pool with its own row,
retention count, and permanent latest-green link.

branches.*.autoBuild stays as a deprecated alias (plain times only);
the unreleased-in-practice v0.9.13 per-slot buildCommand/name syntax is
removed again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 20:06:02 +02:00
mhoennigandClaude Fable 5 5051c7bb99 Propose ADR 0007: build definitions replace branch-owned schedules
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:52:39 +02:00
mhoennigandClaude Fable 5 6f80190581 Record the v0.9.13 deployment to vm4006
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:16:22 +02:00
mhoennigandClaude Fable 5 a7ac0a72c3 Per-slot auto-build commands and named nightly pools (v0.9.13)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:13:12 +02:00
mhoennigandClaude Fable 5 8aee3190ea Let auto-build slots run their own build command under their own name
A branches.<name>.autoBuild.times entry is now either a plain HH:MM
string or an object with time, its own buildCommand, and a name, so a
nightly slot can run a fuller check than the on-commit builds. The
watcher passes the slot's command and name to the executor, persisted in
the build result — UI restarts, gittally retry, and the startup recovery
repeat a build with the command and name it originally ran under.

A named slot (e.g. master@nightly) gets its own pool: repository
grouping, retention count, branches-view row (sorted after its branch),
latest status, and permanent latest-green artifact link are keyed by the
build name, while origin lookups, gone-branch pruning, worktrees, and
Gitea links/statuses stay keyed by the real branch. Without a name,
slot builds share the branch's pool as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:11:35 +02:00
mhoennigandClaude f292badac1 Record the v0.9.12 deployment to vm4006
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 13:36:47 +02:00
mhoennigandClaude 5bbe422734 Visible mid-merge builds and dedup'd triggers (v0.9.12)
Deployment bundle: the prune protection for queued/running builds and the
manual-trigger dedup.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 13:35:17 +02:00
mhoennigandClaude 09ed193ac7 Never prune queued/running builds; dedup manual triggers
Two defects seen live on vm4006 when a merged branch was deleted from origin
while its last build still ran:

- The result prune removed the PENDING/RUNNING entries of branches gone from
  origin, so the executing build vanished from UI and history and the queue
  looked stuck. Prune now never touches a PENDING or RUNNING entry (worktrees
  were already protected). The startup recovery closes out an orphaned PENDING
  of a gone branch as INTERRUPTED, so the new immunity cannot leak entries.

- The apparent hang invited restart clicks, and each click stacked another
  build of the same commit. startBuild now returns the already queued or
  executing build of the same branch and commit instead of a duplicate;
  cancel-requested builds do not block re-queueing, and re-running a finished
  build stays possible.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 13:30:11 +02:00
mhoennigandClaude d2128afb6d Record the v0.9.11 deployment to vm4006
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 12:22:04 +02:00
mhoennigandClaude 349a01183d Fast-forwarded branch refs and failure-marked logs (v0.9.11)
Deployment bundle: the watcher's local-ref fast-forward and the failed badge on
the logs that carry the build tool's failure line.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 12:19:26 +02:00
mhoennigandClaude 9b992c71ed Fast-forward local branch refs at the end of each poll cycle
Build worktrees share the primary checkout's .git, so build tools can read
refs/heads there. Since the rewrite never moved those refs, they stayed frozen at
the last checkout: hs.hsadmin.ng's prQuickCheck compares master with
origin/master and therefore failed every build once origin moved on.

The sync runs after the enqueue decision on purpose — a local ref lagging behind
origin is exactly how the watcher recognizes new commits, so keeping the refs in
sync earlier (cron job, mirroring refspec, or this step moved up) would silence
the branch instead of building it.

Fast-forward only, as a compare-and-swap against the commit just read: diverged
or ahead branches stay untouched, and the checked-out branch is advanced with
merge --ff-only, which refuses to overwrite conflicting uncommitted changes.
Switched off with watcher.fastForwardLocalRefs: false.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 11:29:42 +02:00
mhoennigandClaude 129f38143d Warn that a statusContext rename mid-build strands a pending Gitea status
Renaming gitea.statusContext while a build runs splits that build over two
contexts: the abandoned one keeps its 'build running' entry, and Gitea reports
the commit as pending forever. Gitea cannot delete a commit status, so document
the manual closing POST as the only way out.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 11:17:24 +02:00
mhoennigandClaude 903a87e547 Correct the runtime bundle's glibc rule: the JDK vendor sets the floor
ADR 0006 assumed the bundle inherits the build machine's glibc, which would
have blocked the Hostsharing Managed Webspace (glibc 2.36, dev machine 2.39).
Measuring all 33 ELF files of the produced bundle shows GLIBC_2.15 as the
highest required symbol version: jlink copies Temurin's prebuilt binaries
instead of compiling, so the floor is the JDK vendor's build environment and
the build machine's glibc is irrelevant unless the toolchain resolves to a
distribution-packaged JDK.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:23:15 +02:00
mhoennigandClaude c77de1c725 Record the webspace's bwrap and kernel versions, and the glibc consequence
bubblewrap 0.8.0 covers every option the sandbox design uses; only overlayfs is
missing, which the design does not need. The kernel version, however, points at
Debian 12 and thus a glibc older than the dev machine's, which would break the
jlink runtime bundle on that host -- noted as a check to run before deploying.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:21:00 +02:00
mhoennigandClaude 3f93882dda Record the passing bwrap precondition check on a Managed Webspace
Plan step 17 hinges on unprivileged user namespaces being usable on a
Hostsharing Managed Webspace. The check ran on h68 and passed with all three
expected signals, so the step is viable there and the sandbox design stands.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:19:21 +02:00
mhoennigandClaude 021d97eca8 Mark the logs that carry a failure line on the artifact index
The artifact index listed the stored logs as bare file names, so a red build
gave no hint which of build.log, build.stdout.log and build.stderr.log actually
explains it — with stdout and stderr stored separately, the failure is usually
in only some of them.

Each log of a non-green build is now scanned for an upper-case FAILED/FAILURE,
which covers BUILD FAILED, Maven's BUILD FAILURE and Gradle's per-test
"SomeTest > works() FAILED"; lower-case prose does not count. The scan streams
line by line with an early exit and reads ISO-8859-1, so no byte sequence of a
build log can fail to decode. Logs of a successful build are not scanned at all
— that saves reading megabytes per page view and avoids an alarming badge on a
green build whose log mentions a deliberately failing sub-build.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:11:45 +02:00
mhoennigandClaude cff365767e Plan step 17: run on a dedicated unix user, and cap the unit's resources
Hostsharing recommends assigning domains to separate domain admins rather
than to the package admin, and all their service guides (Mattermost,
Tomcat, Nextcloud) run the daemon as its own user. For GitTally the
argument is stronger: it checks out foreign commits and executes their
build scripts, so running as the package admin would undo the sandbox
rationale of this step. The service user has to be named when ordering
the daemon port anyway.

Also records a trap found on the way: the RAM contingent is a package
slice, not a per-user quota, so a dedicated user buys isolation but no
extra memory — a runaway Gradle build could starve the whole webspace.
The unit from `init --systemd` sets neither MemoryMax nor TasksMax today,
so adding them (configurable, empty = unset) becomes part of step 17.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:52:24 +02:00