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>
This commit is contained in:
Michael Hönnig
2026-08-31 13:32:54 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 4d76744721
commit d0169e57bc
184 changed files with 2237 additions and 7837 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
# Legacy gitTally Analysis
# Legacy Werkator Analysis
Condensed analysis of `legacy/gitTally` (bash, ~6000 lines) as input for the rewrite.
Condensed analysis of the legacy bash script (~6000 lines) as input for the rewrite.
The script was removed from the tree with the rename to Werkator; its last state is `git show 7f55068^:legacy/gitTally`.
Line numbers refer to the legacy script at the time of analysis (version 0.7.8).
## What the Legacy System Does
+3 -3
View File
@@ -9,14 +9,14 @@ A tested domain model for build results plus a persistent repository, replacing
## Design
Create package `de.hoennig.gittally.build`:
Create package `de.hoennig.werkator.build`:
- `BuildStatus` enum: `PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `INTERRUPTED`, `CANCELLED`.
Add `isTerminal`, `isRestartable` (pending/running/interrupted) properties.
- `BuildResult` data class: branch, commit SHA, status, startedAt, duration, artifactKey.
Use `java.time.Instant`/`Duration`; format only at the edges.
- `BuildResultRepository` interface: append, update status of latest entry for a branch, query latest per branch, query history, delete entry, prune.
- `FileBuildResultRepository`: JSON file at `.git/gittally/build-results.json`.
- `FileBuildResultRepository`: JSON file at `.git/werkator/build-results.json`.
Write atomically (write temp file, then `Files.move` with `ATOMIC_MOVE`).
Reuse the Jackson YAML/JSON setup style from `ConfigLoader`.
@@ -52,7 +52,7 @@ Kotest `FunSpec`, no Spring context needed.
## Execution Notes (done 2026-07-07)
Implemented as specified in `de.hoennig.gittally.build`; build green, 19 new tests.
Implemented as specified in `de.hoennig.werkator.build`; build green, 19 new tests.
Deviations and details:
- Added `jackson-datatype-jsr310` to `build.gradle.kts` for `Instant`/`Duration` JSON support (ISO-8601 strings).
+4 -4
View File
@@ -9,7 +9,7 @@ A tested client for the Gitea commit-status API.
## Design
Create package `de.hoennig.gittally.gitea`:
Create package `de.hoennig.werkator.gitea`:
- `GiteaClient` using Spring's `RestClient`.
- `publishStatus(sha, state, description, targetUrl)``POST /api/v1/repos/{owner}/{repo}/statuses/{sha}` with header `Authorization: token <git.token>`; body fields `state`, `context`, `description`, `target_url`.
@@ -19,12 +19,12 @@ Create package `de.hoennig.gittally.gitea`:
- `isEnabled()` — true only when `gitea.baseUrl`, `gitea.owner`, `gitea.repo`, and `git.token` are configured.
All callers must treat a disabled or failing client as non-fatal (log and continue); the legacy behaved the same but failed silently.
Configuration comes from `GitTallyConfig` (`gitea.*`, `git.token`).
Configuration comes from `WerkatorConfig` (`gitea.*`, `git.token`).
## Out of Scope
- No callers yet; the build executor (step 04) wires status publishing.
- No webhook receiving; GitTally remains poll-based.
- No webhook receiving; Werkator remains poll-based.
## Tests
@@ -50,4 +50,4 @@ Implemented as designed; deviations and decisions:
- `resolveUsername` only requires `gitea.baseUrl` and `git.token`; legacy gated it on the full status-enabled check including owner/repo, which the `/api/v1/user` endpoint does not need.
- Responses are read as strings and parsed with a dedicated Jackson `ObjectMapper` instead of RestClient message converters, keeping malformed-JSON handling explicit and independent of converter auto-detection.
- The legacy "Build status deleted" description marker is not ported; it belongs to the result-delete feature of later steps.
- No config changes were needed: `gitea.*` and `git.token` already exist in `GitTallyConfig`, the `init` templates, and `docs/configuration.md`.
- No config changes were needed: `gitea.*` and `git.token` already exist in `WerkatorConfig`, the `init` templates, and `docs/configuration.md`.
+6 -6
View File
@@ -10,7 +10,7 @@ This step fixes the legacy defect that nothing could observe status changes whil
## Design
Create package `de.hoennig.gittally.build` (extends step 01):
Create package `de.hoennig.werkator.build` (extends step 01):
- `BuildExecutor` service; one build at a time (a `ReentrantLock` or single-thread executor replaces the legacy flock file).
- `startBuild(branch, commit)` runs asynchronously and returns immediately; expose `currentBuild(): RunningBuild?`.
@@ -50,7 +50,7 @@ Consider `builds.timeout` only if trivial; otherwise defer.
## Execution Notes (done 2026-07-07)
Implemented as designed in `de.hoennig.gittally.build`; build green, 18 new tests
Implemented as designed in `de.hoennig.werkator.build`; build green, 18 new tests
(`BuildExecutorTest`, `ProcessBuildRunnerTest`, `ArtifactKeysTest`, plus two new `FileBuildResultRepositoryTest` cases).
Deviations and decisions:
@@ -61,10 +61,10 @@ Deviations and decisions:
The flag is checked before each command and after `waitFor`, so a cancel between clean and build commands still records `CANCELLED`.
- Artifact key naming (`ArtifactKeys`) was needed here because `BuildResult` requires a key; it follows the legacy scheme (sanitized name + 12-char SHA-256 prefix + sanitized ISO timestamp + hash) using the UTC `Instant`, not local time.
Step 05 should reuse it rather than re-implement.
- `ArtifactStore` is an interface in the `build` package with a logging `NoOpArtifactStore` placeholder; step 05 replaces the placeholder and implements the real store in `de.hoennig.gittally.artifacts`.
- `ArtifactStore` is an interface in the `build` package with a logging `NoOpArtifactStore` placeholder; step 05 replaces the placeholder and implements the real store in `de.hoennig.werkator.artifacts`.
- The combined live log is `build.log` inside the per-build staging directory (a temp dir exposed via `RunningBuild.stagingDir`/`liveLogFile`); output is flushed per read chunk so the log grows while the build runs.
- Commands run via `bash -c` with the branch name in the environment as `branch`, like legacy `run_build_command`; a failing `cleanCommand` fails the build without running `buildCommand`.
- `BuildResultRepository` is wired as a Spring bean (`BuildConfiguration`) at `.git/gittally/build-results.json` relative to the working directory, matching how `ConfigLoader` resolves the override file; `git rev-parse --git-path` style worktree resolution can come later if needed.
- `BuildResultRepository` is wired as a Spring bean (`BuildConfiguration`) at `.git/werkator/build-results.json` relative to the working directory, matching how `ConfigLoader` resolves the override file; `git rev-parse --git-path` style worktree resolution can come later if needed.
- Gitea `target_url` is not published yet; the artifact page URL scheme only exists from step 07 on.
- `builds.timeout` was deferred (not trivial alongside cancellation semantics); no config keys were added or changed.
@@ -75,9 +75,9 @@ Refactored on request, superseding parts of the notes above:
- Builds now run concurrently up to the new config key `builds.maxConcurrent` (default 1), enforced by a global semaphore sized on first use (changing it requires a restart).
- At most one build per branch at a time, enforced by one serial worker per branch; a second build of the same branch queues as `PENDING` and runs afterwards ("finish, then next").
Whether a new commit should instead cancel the branch's running build is a later, possibly configurable decision (see step 06).
- Each branch builds in its own reusable git worktree at `.git/gittally/worktrees/<branchKey>` (`BranchWorkspaces`/`GitWorktreeWorkspaces`), checked out detached at the requested commit — the primary checkout is never touched.
- Each branch builds in its own reusable git worktree at `.git/werkator/worktrees/<branchKey>` (`BranchWorkspaces`/`GitWorktreeWorkspaces`), checked out detached at the requested commit — the primary checkout is never touched.
Reuse keeps incremental build caches; `cleanCommand` decides how much of them survives.
`GitService` gained `worktreeAdd`, `worktreePrune`, and `checkoutDetached` for this.
- API change: `currentBuild()` became `currentBuilds(): List<RunningBuild>`, and `cancel()` became `cancel(artifactKey)`; a queued build can be cancelled too and is recorded `CANCELLED` when its worker picks it up.
- The Gitea `PENDING` status is now published synchronously in `startBuild`, so queued builds are visible in Gitea while they wait for a slot.
- Branch config is still loaded from the primary repository directory, not from the branch's checked-out `.gittally.yml`; honoring the branch's own committed config would be a separate decision.
- Branch config is still loaded from the primary repository directory, not from the branch's checked-out `.werkator.yml`; honoring the branch's own committed config would be a separate decision.
+6 -6
View File
@@ -9,10 +9,10 @@ Persist build artifacts (logs plus configured report directories) with stable na
## Design
Create package `de.hoennig.gittally.artifacts`:
Create package `de.hoennig.werkator.artifacts`:
- `ArtifactStore` service implementing the interface stubbed in step 04.
- Artifact root: a configurable directory (new key `artifacts.rootDir`), defaulting to `${XDG_STATE_HOME:-~/.local/state}/gittally/artifacts/<repo-key>`.
- Artifact root: a configurable directory (new key `artifacts.rootDir`), defaulting to `${XDG_STATE_HOME:-~/.local/state}/werkator/artifacts/<repo-key>`.
Do NOT default to `/tmp` like legacy — artifacts vanished on reboot.
- Repo key: sanitized absolute repo path (legacy `repository_key`): non `[A-Za-z0-9._-]``_`.
- Artifact key per build: sanitized branch name + 12-char SHA-256 prefix, plus sanitized start timestamp + hash (legacy `build_artifact_key`); keep this scheme so URLs stay predictable.
@@ -28,7 +28,7 @@ Create package `de.hoennig.gittally.artifacts`:
## Config
New key `artifacts.rootDir` (empty = platform default above).
Update `GitTallyConfig`, `InitCommand` templates, and `docs/configuration.md` together.
Update `WerkatorConfig`, `InitCommand` templates, and `docs/configuration.md` together.
## Tests
@@ -43,11 +43,11 @@ Update `GitTallyConfig`, `InitCommand` templates, and `docs/configuration.md` to
## Execution Notes (done 2026-07-07)
Implemented as `FileArtifactStore` in `de.hoennig.gittally.artifacts`; build green, 12 new tests
Implemented as `FileArtifactStore` in `de.hoennig.werkator.artifacts`; build green, 12 new tests
(`FileArtifactStoreTest`, `BuildExecutorArtifactIntegrationTest`, plus a `repoKey` case in `ArtifactKeysTest`).
Deviations and decisions:
- The `ArtifactStore` interface stays in `de.hoennig.gittally.build` (moving it would make `build` depend on `artifacts`).
- The `ArtifactStore` interface stays in `de.hoennig.werkator.build` (moving it would make `build` depend on `artifacts`).
It gained `prune(keptResults)` and `artifactDir(artifactKey)`; the `NoOpArtifactStore` placeholder was removed.
- Interface gap from step 04 resolved by an additional parameter: `persist(build, stagingDir, workspace)`.
The store copies the configured `artifactDirs` out of the branch worktree itself, so the archived layout stays store knowledge.
@@ -67,5 +67,5 @@ Deviations and decisions:
and returns the removed keys.
- `artifactDir` rejects keys outside `[A-Za-z0-9._-]+` and anything resolving outside `<root>/branches/` (path traversal).
- `artifacts.rootDir` supports a leading `~/` and resolves relative paths against the repository;
when empty, the default is `$XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/artifacts/<repo-key>` as designed.
when empty, the default is `$XDG_STATE_HOME` (or `~/.local/state`) + `/werkator/artifacts/<repo-key>` as designed.
- The bean is wired in `ArtifactsConfiguration` with the working directory defaulting to `.`, mirroring `BuildConfiguration`.
+6 -6
View File
@@ -9,7 +9,7 @@ Replace the legacy blocking main loop with a non-blocking, observable scheduler.
## Design
Create package `de.hoennig.gittally.watcher`:
Create package `de.hoennig.werkator.watcher`:
- `Watcher` component with a fixed-delay poll cycle (Spring `@Scheduled` or a managed executor; enabled only in server/watch mode, not during CLI commands or tests).
- One poll cycle, never blocking on a build:
@@ -19,7 +19,7 @@ Create package `de.hoennig.gittally.watcher`:
4. Start builds for due branches via `startBuild(branch, commit)` (async).
The executor prepares a per-branch worktree itself (step 04 amendment) — the watcher must never check out or reset the primary worktree.
Multiple branches may build concurrently (`builds.maxConcurrent`); the executor already serializes builds of the same branch, so the watcher only has to avoid enqueueing a branch that is already pending or running.
5. Run repository retention pruning and artifact pruning; also remove worktrees under `.git/gittally/worktrees/` of branches no longer on origin (`git worktree remove` or delete + `worktreePrune`).
5. Run repository retention pruning and artifact pruning; also remove worktrees under `.git/werkator/worktrees/` of branches no longer on origin (`git worktree remove` or delete + `worktreePrune`).
- Decide here (or defer with a note): when a new commit arrives for a branch whose build is still running, keep the current queue-behind behavior or cancel the running build and start fresh — this may become a per-branch config option.
- Startup sequence (port of legacy recovery): mark stale running builds interrupted, then enqueue restartable branches.
- Auto-builds: per-branch `autoBuild.enabled` + `times` (UTC HH:MM) from the merged `branches` config.
@@ -34,7 +34,7 @@ Create package `de.hoennig.gittally.watcher`:
## Config
New key `watcher.pollInterval` (e.g. `10s`, default matching legacy cadence).
Update `GitTallyConfig`, `InitCommand` templates, and `docs/configuration.md` together.
Update `WerkatorConfig`, `InitCommand` templates, and `docs/configuration.md` together.
## Tests
@@ -51,7 +51,7 @@ Update `GitTallyConfig`, `InitCommand` templates, and `docs/configuration.md` to
## Execution Notes (done 2026-07-07)
Implemented as designed in `de.hoennig.gittally.watcher`; build green, 23 new tests
Implemented as designed in `de.hoennig.werkator.watcher`; build green, 23 new tests
(`WatcherTest`, `AutoBuildStateTest`, plus new `DurationParserTest` and `GitServiceTest` cases).
Deviations and decisions:
@@ -72,7 +72,7 @@ Deviations and decisions:
- Enqueue precedence per cycle: changed local branches, then recent new origin branches, then due auto-build slots;
each branch at most once (an auto-build slot stays untriggered while its branch is pending/running and fires
on a later cycle instead of being lost).
- Auto-build state lives in `.git/gittally/auto-builds.json` (`FileAutoBuildState`, replaces `auto-builds.tsv`);
- Auto-build state lives in `.git/werkator/auto-builds.json` (`FileAutoBuildState`, replaces `auto-builds.tsv`);
entries of past days are dropped on write. Slot matching (`AutoBuildSlots`) picks the latest slot at or before
the current UTC time, like legacy `auto_build_check`. Only branches named in the `branches` config
(other than `default`) can auto-build; `default.autoBuild.enabled` does not extend to unlisted branches.
@@ -80,7 +80,7 @@ Deviations and decisions:
on origin, after a best-effort fetch (a failing fetch recovers from the last known origin state).
A stale latest PENDING entry is marked INTERRUPTED before its replacement build is enqueued,
because the executor queue does not survive a restart.
- Worktree cleanup deletes `.git/gittally/worktrees/<branchKey>` directories of branches gone from origin
- Worktree cleanup deletes `.git/werkator/worktrees/<branchKey>` directories of branches gone from origin
(never those of queued or running builds) and then calls `git worktree prune`.
- `DurationParser` was extended with `s`/`m` suffixes for `watcher.pollInterval` (it only knew `d`/`h`).
- Watcher health is exposed via `Watcher.state(): WatcherState` (running, last poll time, last fetch/poll error,
+3 -3
View File
@@ -18,7 +18,7 @@ Bootstrapping:
- The watcher (step 06) is active only in the `server` profile.
- New config keys `server.port` and `server.bindAddress` (defaults 18080 / 0.0.0.0, as legacy).
JSON API (package `de.hoennig.gittally.server`), replacing the legacy `/control/*` endpoints:
JSON API (package `de.hoennig.werkator.server`), replacing the legacy `/control/*` endpoints:
- `GET /api/builds/latest` — latest build per branch.
- `GET /api/builds/history` — all builds, newest first.
@@ -54,13 +54,13 @@ After Ctrl-C, `ServerCommand` parks the command thread while the JVM shuts down;
Deviations and decisions:
- The live log tail is a sibling endpoint: `GET /api/builds/current` lists the running builds (with `logSize`), and `GET /api/builds/current/{artifactKey}/log?offset=` fetches the log incrementally, addressed by artifact key as required. Responses are capped at 1 MiB per chunk.
- The control token needs no config key. It is generated on first use and persisted to `.git/gittally/control-token` (mode 600); operators can write their own token there, deleting the file rotates it. Requests pass it via the `X-GitTally-Token` header or a `token` parameter; mismatch answers 403 like legacy.
- The control token needs no config key. It is generated on first use and persisted to `.git/werkator/control-token` (mode 600); operators can write their own token there, deleting the file rotates it. Requests pass it via the `X-werkator-Token` header or a `token` parameter; mismatch answers 403 like legacy.
- `DELETE /api/builds/{artifactKey}` removes the result and then calls `ArtifactStore.prune(history)`, so no new store interface method was needed.
- `GET /api/status/{commit}` also accepts abbreviated hashes (740 hex like legacy) and resolves them against the local history. The `GiteaClient` (step 03) gained 10s connect/read timeouts so the endpoint can never hang; a Gitea failure yields HTTP 200 with `status: unknown` (or the local status) plus `giteaError`.
- `POST /api/builds/{branch}/restart` rebuilds the branch's last recorded commit. Branch names containing `/` would need an encoded slash, which Tomcat rejects by default — revisit in step 08 if the UI needs restart for such branches. (Resolved in step 08: the endpoint moved to `POST /api/builds/restart?branch=…`.)
- Spring Boot 4 moved `@WebMvcTest` into the new `spring-boot-starter-webmvc-test` test module (added as test dependency).
- The server-profile `@SpringBootTest` mocks the `Watcher` bean, so booting the test never fetches origin or enqueues builds; watcher wiring is proven by verifying `start()` was called.
Manual smoke test (2026-07-07): in a scratch repository, `java -jar build/libs/gittally-0.1.0-SNAPSHOT.jar server` started on the configured port 18981.
Manual smoke test (2026-07-07): in a scratch repository, `java -jar build/libs/werkator-0.1.0-SNAPSHOT.jar server` started on the configured port 18981.
`GET /api/builds/latest` answered `[]` with HTTP 200, `GET /api/watcher` exposed the failing fetch of the origin-less repo as `lastFetchError`, `GET /api/status/<sha>` answered `unknown` with HTTP 200, and cancel without token answered 403.
SIGINT (Ctrl-C) shut the process down cleanly in about 2 seconds: port closed, no exceptions in the log, exit code 130.
+5 -5
View File
@@ -48,7 +48,7 @@ Robust live updates (the actual bug fix):
## Implementation Notes (2026-07-07)
Implemented as designed: Thymeleaf templates (`fragments`, `builds`, `current`, `artifact`) rendered by `UiController`, one hand-written `static/gittally.js`, one `static/gittally.css` (loosely ported legacy look incl. dark mode and the mobile card layout), and the legacy favicon.
Implemented as designed: Thymeleaf templates (`fragments`, `builds`, `current`, `artifact`) rendered by `UiController`, one hand-written `static/werkator.js`, one `static/werkator.css` (loosely ported legacy look incl. dark mode and the mobile card layout), and the legacy favicon.
Pages render the full state server-side and work without JavaScript; the script polls the JSON API (tables 10s, current builds and log tails 3s) and re-renders table bodies from data.
Every fetch runs with an 8s timeout; a failure flips the nav-row indicator to an explicit `error` badge and dims the stale table — there is no loading state at all, so no spinner can get stuck.
Polling pauses on `visibilitychange` and refreshes immediately when the tab becomes visible; running durations tick client-side from `data-started-at`.
@@ -57,7 +57,7 @@ Deviations and decisions:
- The tables show one `Started` column instead of the legacy `Commit Time` + `Status Time` pair, and client-side column sorting was not ported; the API delivers newest-first.
- Status badges show the repository status; the per-row Gitea lookup (`/control/status` per commit) was deliberately not ported — that fan-out caused the legacy stuck spinners. `GET /api/status/{commit}` remains available.
- The control token is embedded as a `<meta>` tag in every rendered page (legacy embedded its cancel token in the cancel form the same way); `gittally.js` sends it via `X-GitTally-Token` for restart/cancel/delete.
- The control token is embedded as a `<meta>` tag in every rendered page (legacy embedded its cancel token in the cancel form the same way); `werkator.js` sends it via `X-werkator-Token` for restart/cancel/delete.
- The restart endpoint moved from `POST /api/builds/{branch}/restart` to `POST /api/builds/restart?branch=…` so branch names with slashes work (resolves the step 07 deviation note).
- Artifact links render whenever a result has an artifact key; the artifact index page itself explains a pruned/missing artifact directory instead of a per-row existence check.
- `/builds/{artifactKey}` renders logs (top-level files) and the topmost `reports/**/index.html` pages from the artifact store; nested index pages below an already-listed one are skipped like legacy. Raw directory browsing is not offered.
@@ -65,7 +65,7 @@ Deviations and decisions:
- On `/current`, a build that leaves the running list keeps its card, marked `finished` with a link to its result page; its initial server render shows an empty log (the script fetches from offset 0).
- JS builds all DOM via `createElement`/`textContent`, so re-rendered data cannot inject markup; server-side escaping is covered by a MockMvc test with a hostile branch name.
- New config key `server.impressumUrl` (empty hides the footer link); the footer version comes from Spring Boot `buildInfo()` (`BuildProperties`, build time excluded for repeatability) with a `dev` fallback.
- `UiFormats` (Kotlin) and `gittally.js` intentionally produce the same timestamp/duration display formats.
- `UiFormats` (Kotlin) and `werkator.js` intentionally produce the same timestamp/duration display formats.
Manual smoke test (2026-07-07): scratch repository with a bare origin, `pollInterval: 5s`, and a 25s build command; server on port 18982, observed through a real browser tab.
After `git push`, the open Latest tab showed the new build without reload and its badge flipped `running``success` live (`pending` was too short to sample; the row itself appeared via polling).
@@ -81,9 +81,9 @@ Deviation: the listing enumerates origin branches instead of legacy's local bran
Addendum (2026-07-07): the legacy per-page reload button (`⟳`, top right) was also re-added on request, next to the live indicator.
On polling pages it triggers an immediate data refresh via the page's poller; pages without a poller (artifact index) reload fully.
Addendum (2026-07-07): all links that leave the GitTally UI open in a new tab (`target="_blank" rel="noopener noreferrer"`).
Addendum (2026-07-07): all links that leave the Werkator UI open in a new tab (`target="_blank" rel="noopener noreferrer"`).
This already held for Gitea branch/commit links and the footer; it was added for the artifact page's log and report links, whose targets have no navigation.
Links between GitTally pages (nav, artifact index) stay in the same tab.
Links between Werkator pages (nav, artifact index) stay in the same tab.
Addendum (2026-08-10): the artifacts column carries the whole build-reachability logic, and the nav lost its `Current` entry.
The permanent `🔗` link is rendered on the build it resolves to — the branch's latest green build — on every build table, instead of on each row of a branch with any green build.
+4 -4
View File
@@ -9,7 +9,7 @@ Port the legacy system page: CPU, RAM, disk, and repository size with min/max/av
## Design
Create package `de.hoennig.gittally.metrics`:
Create package `de.hoennig.werkator.metrics`:
- `SystemMetricsCollector` sampling every 60s (server profile only):
CPU used/idle from `/proc/stat` deltas, RAM from `/proc/meminfo`, disk from `java.nio.file.FileStore`, repo size via periodic `du -sk` (or a file walk) — throttle repo-size sampling (legacy ran `du` every cycle, which was expensive).
@@ -36,10 +36,10 @@ Create package `de.hoennig.gittally.metrics`:
## Implementation Notes (2026-07-07)
Implemented as designed: `SystemMetricsCollector` in `de.hoennig.gittally.metrics` samples every 60s once `ServerMetricsLifecycle` (server profile only) calls `start()`, following the watcher's start/stop pattern.
Implemented as designed: `SystemMetricsCollector` in `de.hoennig.werkator.metrics` samples every 60s once `ServerMetricsLifecycle` (server profile only) calls `start()`, following the watcher's start/stop pattern.
CPU comes from `/proc/stat` deltas, RAM from `/proc/meminfo`, disk from `java.nio.file.FileStore` (`df` semantics: used = total unallocated, free = usable), and the repository size from a file walk.
`GET /api/system` returns the snapshot plus aggregates, and `/system` renders the legacy system page in the step 08 layout, polling every 60s with the same timeout/error-badge rules.
Since the metric rows are fixed, `gittally.js` only updates the cell texts in place — nothing is rebuilt.
Since the metric rows are fixed, `werkator.js` only updates the cell texts in place — nothing is rebuilt.
Deviations and decisions:
@@ -51,7 +51,7 @@ Deviations and decisions:
- The repository size is re-probed only every 10th sample (10 minutes) and reused in between — the throttle this step requires; legacy ran `du -sk` every cycle.
The file walk sums file sizes, not disk blocks like `du`, which is close enough for a trend metric.
- An unavailable source (no `/proc` outside Linux, unreadable file store) yields explicit `null` metrics over HTTP 200 and `n/a` cells; the failure is logged once, not every 60s.
- No new config keys: the 60s interval is fixed like legacy, so `GitTallyConfig`, the `init` templates, and `docs/configuration.md` are unchanged.
- No new config keys: the 60s interval is fixed like legacy, so `WerkatorConfig`, the `init` templates, and `docs/configuration.md` are unchanged.
- The legacy `generation` field was not ported; it only guarded the legacy JS against monitor restarts.
- The CPU count comes from `Runtime.availableProcessors()` instead of `nproc`.
+2 -2
View File
@@ -39,7 +39,7 @@ Exit codes: 0 on success, 1 on build failure, 2 on usage/config errors (align wi
## Implementation Notes (2026-07-07)
Implemented as designed: `status`, `build`, and `retry` are picocli `@Component` subcommands in `commands/`, wired into `GitTallyCommand` like the existing ones.
Implemented as designed: `status`, `build`, and `retry` are picocli `@Component` subcommands in `commands/`, wired into `WerkatorCommand` like the existing ones.
They implement `Callable<Int>`, so the exit codes align with `CliRunner`'s `ExitCodeGenerator` contract: 0 on success, 1 on build failure, 2 on usage/config errors (picocli's own `USAGE` code for invalid options matches).
- `status [--history]` reads `BuildResultRepository` directly and prints an aligned table (branch, status, commit, time, duration); it reuses `UiFormats`, so the console shows the same timestamp/duration formats as the web UI.
@@ -56,7 +56,7 @@ Deviations and decisions:
- A failed fetch only warns and the commands continue from the last-known origin state, so they work offline.
- `retry` only retries FAILED builds (legacy `branch_has_failed_build` checked exactly `failed`); interrupted/pending builds are the watcher's startup-recovery job.
- Exit code 130 for cancelled builds was not ported; a cancelled/interrupted build exits 1 like any non-success.
- Found while smoke testing: `.gitignore`'s `*.jar` rule excluded `gradle/wrapper/gradle-wrapper.jar`, so builds in fresh checkouts — including every GitTally worktree — failed with `ClassNotFoundException: GradleWrapperMain`.
- Found while smoke testing: `.gitignore`'s `*.jar` rule excluded `gradle/wrapper/gradle-wrapper.jar`, so builds in fresh checkouts — including every Werkator worktree — failed with `ClassNotFoundException: GradleWrapperMain`.
Fixed with a `!gradle/wrapper/gradle-wrapper.jar` exception and by adding the jar (same class of defect as the `build/` rule fixed in step 04).
Manual smoke test (2026-07-07, in this repository):
+10 -10
View File
@@ -14,18 +14,18 @@ Implement a `DockerBuildRunner` for the `BuildRunner` interface from step 04, sh
Port from legacy (see analysis, lines ~4400+):
- Ensure image: build from configured Dockerfile/context when missing or stale; track staleness via an image label holding the SHA-256 of Dockerfile + context (legacy `org.gittally.build-inputs-sha256`).
- Gradle cache volume per repository (`gittally-gradle-<repo-key>`), mounted and chowned to the host UID/GID.
- Ensure image: build from configured Dockerfile/context when missing or stale; track staleness via an image label holding the SHA-256 of Dockerfile + context (legacy `org.werkator.build-inputs-sha256`).
- Gradle cache volume per repository (`werkator-gradle-<repo-key>`), mounted and chowned to the host UID/GID.
- Run the build container: workspace mount, branch env var, configured extra env, network mode, docker socket mount for Testcontainers-based builds.
- Post-build ownership repair of the workspace (legacy `repair_docker_workspace_ownership`).
- Label all containers (`org.hoennig.gittally=true`, repository, role) and clean up stale ones on startup.
- Label all containers (`org.hoennig.werkator=true`, repository, role) and clean up stale ones on startup.
Decide during implementation whether the hsadmin-ng-specific legacy options (preflight command, `JAVA_TOOL_OPTIONS` injection) are needed; default to NOT porting them (see orphaned-config list in the analysis).
## Config
New `branches.<name>.docker` section: `enabled`, `image`, `dockerfile`, `context`, `network`, `env`.
Update `GitTallyConfig`, `InitCommand` templates, and `docs/configuration.md` together.
Update `WerkatorConfig`, `InitCommand` templates, and `docs/configuration.md` together.
## Tests
@@ -46,19 +46,19 @@ No test needs Docker; the main `docker run` argv is asserted exactly through an
Ported from legacy:
- Image ensure (`ensure_docker_build_image`): rebuild when the image is missing or the `org.gittally.build-inputs-sha256` label no longer matches; all four `org.gittally.*` labels are set.
- Image ensure (`ensure_docker_build_image`): rebuild when the image is missing or the `org.werkator.build-inputs-sha256` label no longer matches; all four `org.werkator.*` labels are set.
Without a configured `dockerfile`, the image is used as-is and pulled by `docker run` on demand.
- Gradle cache volume `gittally-gradle-<repo-key>`, created and chowned to the host uid/gid with the legacy container script.
- Gradle cache volume `werkator-gradle-<repo-key>`, created and chowned to the host uid/gid with the legacy container script.
- Build container: workspace bind mount, `branch` env var, configured extra env, network mode, docker socket mount with `DOCKER_HOST`/`TESTCONTAINERS_*` for Testcontainers-based builds, `--add-host host.docker.internal:host-gateway` off host network.
- Ownership repair of `build/` and `.gradle/` (`repair_docker_workspace_ownership`).
- `org.hoennig.gittally` labels (role `build`) and stale-container cleanup.
- `org.hoennig.werkator` labels (role `build`) and stale-container cleanup.
Deviations and decisions:
- The `BuildRunner` interface gained `repoDir` and `branchConfig` parameters (with defaults), because runner selection and Docker settings are per branch and the per-repo volume/container names need the repository path — a worktree cannot resolve the uncommitted config layer.
- The hsadmin-ng-specific legacy options were not ported, as the step suggests: no preflight command, no `JAVA_TOOL_OPTIONS` injection, no `.testcontainers.properties` generation, no `HSADMINNG_*` env passthrough — `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE`/`TESTCONTAINERS_HOST_OVERRIDE`/`DOCKER_HOST` cover modern Testcontainers; anything else fits `docker.env`.
- Ownership repair runs inside the same build container (wrapped around the command, preserving its exit code) instead of a follow-up root container; the separate `prepare_docker_workspace_build_dir` step became unnecessary because the clean command already runs in the container.
- Container names are per branch (`gittally-build-<repo-key>-<branch-key>`), not per repository, because builds of different branches may run concurrently.
- Container names are per branch (`werkator-build-<repo-key>-<branch-key>`), not per repository, because builds of different branches may run concurrently.
- Containers run with `--init`, so termination signals from build cancellation reach the build process inside the container.
- Stale labelled containers are removed before the first Docker build of the process, not at daemon startup, so installations that never build in Docker never invoke docker.
- The Gradle volume is prepared once per process and image, not before every build.
@@ -68,8 +68,8 @@ Deviations and decisions:
Manual smoke test (2026-07-07, scratch repo, Rancher Desktop 27.3.1):
- A scratch repo with `docker.enabled`, a two-line Dockerfile, and a build command writing `id -u` into `build/who.txt`: `gittally build` built the image with all four labels, created the `gittally-gradle-<repo-key>` volume, streamed the container output live, and exited 0 (`success after 0:14`).
- A scratch repo with `docker.enabled`, a two-line Dockerfile, and a build command writing `id -u` into `build/who.txt`: `werkator build` built the image with all four labels, created the `werkator-gradle-<repo-key>` volume, streamed the container output live, and exited 0 (`success after 0:14`).
- A second run reused the image (inputs label matched, no rebuild; `success after 0:04`).
- The command ran as uid 0 inside the container while `build/who.txt` ended up owned by the host user — the in-container ownership repair works.
- No labelled containers were left behind after the builds.
- Caveat found while testing (environmental, not GitTally): with a VM-based Docker (Rancher Desktop/Lima), workspace bind mounts only work for paths shared into the VM (e.g. `$HOME`); a repo under an unshared `/tmp` builds against an empty VM-side directory.
- Caveat found while testing (environmental, not Werkator): with a VM-based Docker (Rancher Desktop/Lima), workspace bind mounts only work for paths shared into the VM (e.g. `$HOME`); a repo under an unshared `/tmp` builds against an empty VM-side directory.
+12 -11
View File
@@ -5,13 +5,13 @@ Read `README.md` and `00-legacy-analysis.md` first.
## Goal
Make the new GitTally deployable as a service and retire the legacy script.
Make the new Werkator deployable as a service and retire the legacy script.
## Design
Deployment (documentation plus a small generator, no self-install):
- Extend `init` (or add `init --systemd`) to generate a systemd user unit running `java -jar gittally.jar server` with `WorkingDirectory` set to the repo, `Restart=always`, and an `EnvironmentFile` for overrides — port the shape of the legacy unit, drop the self-copy/update machinery.
- Extend `init` (or add `init --systemd`) to generate a systemd user unit running `java -jar werkator.jar server` with `WorkingDirectory` set to the repo, `Restart=always`, and an `EnvironmentFile` for overrides — port the shape of the legacy unit, drop the self-copy/update machinery.
- Write `docs/deployment.md`: JRE requirement, jar location convention, systemd enable/start/log commands, and reverse-proxy guidance (example nginx `server` block proxying to `server.port`; TLS via the host's existing certbot — replaces the legacy managed nginx container).
Migration:
@@ -21,8 +21,8 @@ Migration:
Housekeeping:
- Mark `legacy/gitTally` as deprecated in its header comment and in `README.md`.
- Review `docs/GitTally-Konzept.md` against what was actually built; update or note deviations.
- Mark `legacy/werkator` as deprecated in its header comment and in `README.md`.
- Review `../Werkator-Konzept.md` against what was actually built; update or note deviations.
- Add an ADR summarizing the architecture decisions that emerged during the rewrite (persistence choice, polling UI, no nginx management).
## Tests
@@ -37,25 +37,26 @@ Housekeeping:
## Implementation Notes (2026-07-07)
Implemented as designed: `init --systemd` (an option on `init`, not a separate subcommand) generates the unit and its `EnvironmentFile` under `.git/gittally/`, prints the install commands, and never touches `~/.config/systemd` itself (no self-install).
Implemented as designed: `init --systemd` (an option on `init`, not a separate subcommand) generates the unit and its `EnvironmentFile` under `.git/werkator/`, prints the install commands, and never touches `~/.config/systemd` itself (no self-install).
`SystemdServiceFiles` builds the file contents and is unit-tested by content assertions, including the legacy `%` escaping and `ExecStart` quoting.
`docs/deployment.md` and `docs/migration-from-legacy.md` were written; `README.md`, `docs/bootstrapping.md`, `docs/GitTally-Konzept.md`, and `CLAUDE.md` were updated to reference them.
`docs/deployment.md` and `docs/migration-from-legacy.md` were written; `README.md`, `docs/bootstrapping.md`, `../Werkator-Konzept.md`, and `CLAUDE.md` were updated to reference them.
`docs/migration-from-legacy.md` was deleted again on 2026-08-30 with the rename to Werkator: every host it addressed had long since moved to the YAML configuration.
Deviations and decisions:
- The unit is named per repository (`gittally-<repo-name>.service`) instead of the global legacy `gitTally.service`, because one instance serves one repository and several repositories can share a host.
- The unit is named per repository (`werkator-<repo-name>.service`) instead of the global legacy `werkator.service`, because one instance serves one repository and several repositories can share a host.
- `ExecStart` uses the `java` binary and the jar path of the JVM that ran `init --systemd`, so the unit points at the jar in place (legacy copied the script to an install dir); systemd expands `$JAVA_OPTS` from the `EnvironmentFile` into the command line.
When not started via `java -jar` (e.g. from Gradle), `init --systemd` prints an error instead of generating a broken unit.
- The `EnvironmentFile` only tunes the JVM (`JAVA_OPTS`); the legacy env file carried username/token, which now live in `.git/gittally/.gittally.yml`.
An existing `gittally.env` is kept; the unit file is regenerated on every run (same as legacy).
- The `EnvironmentFile` only tunes the JVM (`JAVA_OPTS`); the legacy env file carried username/token, which now live in `.git/werkator/.werkator.yml`.
An existing `werkator.env` is kept; the unit file is regenerated on every run (same as legacy).
- The legacy `--nginx --docker` `ExecStart` flags were dropped (runtime selection is per-branch config now); the `After=… docker.service` ordering was kept.
- Legacy build history (`build-results.tsv`) is not imported — decided and documented in `docs/migration-from-legacy.md` (formats differ substantially; retention would prune imported rows quickly).
- ADR 0004 records the rewrite architecture decisions (JSON-file persistence behind a repository interface, polling UI, no managed nginx).
- `docs/GitTally-Konzept.md` review: only one real deviation found — "Builds laufen in Docker" became "nativ oder optional in Docker (pro Branch konfigurierbar)"; CLI capability lists gained build/retry; deployment links added.
- `../Werkator-Konzept.md` review: only one real deviation found — "Builds laufen in Docker" became "nativ oder optional in Docker (pro Branch konfigurierbar)"; CLI capability lists gained build/retry; deployment links added.
Manual walkthrough (2026-07-07, fresh clone under `~/.cache`):
- Followed `docs/deployment.md` end to end: built the jar, copied it to a stable path, cloned the repository freshly, ran `init` and `init --systemd`, linked the generated unit, `daemon-reload`, started the service.
- The clone already contained the committed `.gittally.yml`, so only the machine config was created; `server.port` was overridden to a free port via `.git/gittally/.gittally.yml` to avoid clashing with a locally running instance.
- The clone already contained the committed `.werkator.yml`, so only the machine config was created; `server.port` was overridden to a free port via `.git/werkator/.werkator.yml` to avoid clashing with a locally running instance.
- Result: unit `active (running)`, `GET /` returned 200, `/api/watcher` showed a successful poll, `journalctl --user -u …` showed the startup log, `restart` and `stop` worked; the unit link and the scratch clone were removed afterwards.
- Not machine-verified: `systemctl --user enable` and `loginctl enable-linger` (the walkthrough used a transient `start` to leave no persistent service behind) and the nginx/certbot section (no public host available); those commands were reviewed against the systemd/certbot documentation instead.
+8 -8
View File
@@ -2,22 +2,22 @@
Prerequisites: steps 07, 11, 12.
Read `README.md`, `00-legacy-analysis.md`, and ADR 0005 first.
Consult `legacy/gitTally` for the functions referenced below.
Consult `legacy/werkator` for the functions referenced below.
## Goal
Serve GitTally over HTTPS on hosts that provide Docker but no host reverse proxy (e.g. Hostsharing managed container environments).
GitTally optionally manages an nginx Docker container with Let's Encrypt certificates, ported from the legacy subsystem.
Serve Werkator over HTTPS on hosts that provide Docker but no host reverse proxy (e.g. Hostsharing managed container environments).
Werkator optionally manages an nginx Docker container with Let's Encrypt certificates, ported from the legacy subsystem.
This is opt-in; the reverse-proxy deployment from step 12 stays the default (ADR 0005).
## Design
Port the legacy nginx subsystem (functions `configure_artifact_nginx_defaults` ~1545, `artifact_nginx_write_ssl_options` ~4051, `artifact_nginx_write_config` ~4077, `artifact_nginx_ports_free` ~4242, `cleanup_stale_artifact_nginx_containers` ~4274, `artifact_nginx_run_container` ~4281, `artifact_nginx_obtain_or_renew_certificate` ~4310, `start_artifact_nginx` ~4342, shutdown cleanup ~723):
- Config under `server.nginx.*`: `enabled` (default false), `serverName`, `httpPort` (8080), `httpsPort` (8443), `upstreamHost` (default: `serverName`), `containerName` (default: `gittally-nginx-<repo-name>`), `stateDir` (default: `${XDG_STATE_HOME:-~/.local/state}/gittally/nginx/<repoKey>`), `letsencryptEmail`.
Update all three config places (`GitTallyConfig`, `init` templates, `docs/configuration.md`).
- Config under `server.nginx.*`: `enabled` (default false), `serverName`, `httpPort` (8080), `httpsPort` (8443), `upstreamHost` (default: `serverName`), `containerName` (default: `werkator-nginx-<repo-name>`), `stateDir` (default: `${XDG_STATE_HOME:-~/.local/state}/werkator/nginx/<repoKey>`), `letsencryptEmail`.
Update all three config places (`WerkatorConfig`, `init` templates, `docs/configuration.md`).
- When `server.publicBaseUrl` is empty and `serverName` is set, default it to `https://<serverName>/` (legacy line ~541).
- Shell out to the `docker` CLI like `DockerBuildRunner` (no SDK); label the container `org.hoennig.gittally` for stale-container cleanup.
- Shell out to the `docker` CLI like `DockerBuildRunner` (no SDK); label the container `org.hoennig.werkator` for stale-container cleanup.
- Lifecycle as a server-profile component (like `ServerWatcherLifecycle`/`ServerMetricsLifecycle`): start after the web server is up, stop and remove the container on shutdown.
Nothing runs in CLI mode or tests.
- Two-phase startup, ported from legacy: write an HTTP-only nginx config for the ACME webroot challenge, run the container, obtain the certificate via a certbot container (webroot mode), then rewrite the full HTTPS config and restart nginx.
@@ -36,7 +36,7 @@ Port the legacy nginx subsystem (functions `configure_artifact_nginx_defaults` ~
- `./gradlew ktlintFormat` then `./gradlew build` is green.
- With `server.nginx.enabled: false` (default) nothing changes; no container is touched.
- Manual walkthrough on a Docker host: nginx container starts with the init config and proxies HTTP to GitTally.
- Manual walkthrough on a Docker host: nginx container starts with the init config and proxies HTTP to werkator.
Full ACME issuance needs a public DNS name; if none is available, verify the certbot argv and the full-config path against the legacy script and document that in this file.
- `docs/deployment.md` gains a section for hosts without a reverse proxy; `docs/migration-from-legacy.md` maps the `GITTALLY_ARTIFACT_NGINX_*`/`GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL` variables.
@@ -50,7 +50,7 @@ Deviations from the design above and from legacy:
- Legacy auto-moved the artifact server port on a collision with the nginx ports; the rewrite refuses to start the proxy with a warning instead — the Spring port cannot move after startup.
- Legacy derived a missing `serverName` from the public base URL host; the rewrite requires `serverName` explicitly (the config default direction is only publicBaseUrl ← serverName).
- `serverName` and `upstreamHost` are validated against a host-name pattern instead of substituting raw values, so no nginx directives can be injected via config.
- The container label namespace is `org.hoennig.gittally` (like the Docker build runner), not `org.hostsharing.gittally`; port cleanup still also matches legacy-named containers.
- The container label namespace is `org.hoennig.werkator` (like the Docker build runner), not `org.hostsharing.werkator`; port cleanup still also matches legacy-named containers.
- The legacy `--nginx` CLI flag is not ported; enablement is `server.nginx.enabled` only.
- `ssl-dhparams.pem` is downloaded via the Java HTTP client instead of `curl` (replaceable seam for tests).
@@ -1,7 +1,7 @@
# Step 14: Build-Phase Timing and Orchestration Overhead
Prerequisites: steps 04 (build executor), 05 (artifact store), 07 (API), 08 (web UI), 11 (Docker runtime).
Read `README.md` first; read the referenced `legacy/gitTally` functions only where this step points at them.
Read `README.md` first; read the referenced legacy functions only where this step points at them — the script lives in the history now, see `README.md`.
## Goal
+63 -57
View File
@@ -6,8 +6,8 @@ This step revises the "Future: Docker-based Deployment" section of `docs/bootstr
## Goal
Deploy GitTally on hosts that provide Docker and git but no Java runtime (Hostsharing container servers, e.g. `tallyman@vm4006`).
GitTally is distributed as a self-contained runtime bundle: a jlink-trimmed JRE plus `gittally.jar` plus a launcher script, packed as one tarball.
Deploy Werkator on hosts that provide Docker and git but no Java runtime (Hostsharing container servers, e.g. `tallyman@vm4006`).
Werkator is distributed as a self-contained runtime bundle: a jlink-trimmed JRE plus `werkator.jar` plus a launcher script, packed as one tarball.
The JAR stays the primary artifact for development and for hosts that already have a JRE.
## Distribution Format Decision (ADR 0006)
@@ -17,9 +17,9 @@ Three formats were considered; write ADR 0006 recording the decision and this ra
- **jlink runtime bundle (chosen)** — no production-code changes, plain JVM semantics, one tarball to `scp`.
git and docker CLIs are used from the host, worktree paths stay host paths, and the `init --systemd` unit works unchanged because `java.home` and the running-jar path resolve into the bundle.
- **GraalVM native image (rejected)** — Spring AOT evaluates bean conditions at build time.
GitTally's dual-context design (CLI context without web, second `SpringApplication` with the `server` profile and `WebApplicationType.SERVLET`, `@Profile("!server")` `CliRunner`, `@Profile("server")` lifecycles) cannot be represented in a single AOT arrangement.
Werkator's dual-context design (CLI context without web, second `SpringApplication` with the `server` profile and `WebApplicationType.SERVLET`, `@Profile("!server")` `CliRunner`, `@Profile("server")` lifecycles) cannot be represented in a single AOT arrangement.
Supporting it would require replacing the profile wiring with runtime guards and collapsing the two context shapes — an invasive rewrite with regression risk for the JVM path.
- **Containerized GitTally runtime (rejected, was the `docs/bootstrapping.md` sketch)** — needs git and docker CLIs inside the image, a same-path `$HOME` mount plus docker-socket mount and uid/gid mapping so that `DockerBuildRunner`'s `--volume $workspace:$workspace` sibling mounts keep working, and a hand-edited systemd unit.
- **Containerized Werkator runtime (rejected, was the `docs/bootstrapping.md` sketch)** — needs git and docker CLIs inside the image, a same-path `$HOME` mount plus docker-socket mount and uid/gid mapping so that `DockerBuildRunner`'s `--volume $workspace:$workspace` sibling mounts keep working, and a hand-edited systemd unit.
Kept as the documented fallback if the bundle approach ever becomes unworkable.
## Target Host Facts (verified 2026-08-10)
@@ -35,15 +35,15 @@ Gradle:
- Add a `runtimeBundle` task (depends on `bootJar`); the normal `./gradlew build` stays unchanged.
- The task runs `jlink` from the configured Java toolchain (every JDK 21 ships jlink; no new toolchain requirement).
- The JDK module list is pinned in the build script, computed once via `jdeps` on the exploded boot jar and its `BOOT-INF/lib`; document the jdeps command next to the list and re-check it when dependencies change.
- Bundle layout: `gittally/jre/` (jlink image), `gittally/lib/gittally.jar`, `gittally/bin/gittally` (sh launcher: `exec "$DIR/../jre/bin/java" $JAVA_OPTS -jar "$DIR/../lib/gittally.jar" "$@"`).
- Output: `build/distributions/gittally-runtime-linux-x64.tar.gz` with preserved execute permissions.
- Bundle layout: `werkator/jre/` (jlink image), `werkator/lib/werkator.jar`, `werkator/bin/werkator` (sh launcher: `exec "$DIR/../jre/bin/java" $JAVA_OPTS -jar "$DIR/../lib/werkator.jar" "$@"`).
- Output: `build/distributions/werkator-runtime-linux-x64.tar.gz` with preserved execute permissions.
Deployment (no code changes expected):
- Unpack to `~/opt/gittally/` on the target host; run everything via `~/opt/gittally/bin/gittally`.
- `init --systemd` already generates `ExecStart=<java> $JAVA_OPTS -jar <jar> server` from `java.home` and the running jar path — from the bundle both resolve into `~/opt/gittally/`, so the unit points at the bundle without changes.
- Unpack to `~/opt/werkator/` on the target host; run everything via `~/opt/werkator/bin/werkator`.
- `init --systemd` already generates `ExecStart=<java> $JAVA_OPTS -jar <jar> server` from `java.home` and the running jar path — from the bundle both resolve into `~/opt/werkator/`, so the unit points at the bundle without changes.
Verify this instead of adapting code; adapt only if the resolution fails.
- Updating GitTally = unpack a new bundle over `~/opt/gittally/` (or switch a symlink) and restart the service.
- Updating Werkator = unpack a new bundle over `~/opt/werkator/` (or switch a symlink) and restart the service.
Documentation:
@@ -55,13 +55,13 @@ Documentation:
## Tests
- No production code changes are expected, so no new unit tests; existing tests must stay green.
- Smoke-verify the bundle manually: `bin/gittally --help`, `init` in a scratch repo, `config:print --full`, a short `server` run, and `init --systemd` unit content pointing into the bundle; document the results in this file.
- Verify on vm4006 (which has no Java): copy the bundle, run `bin/gittally --help` and `config:print`; document the results in this file.
- Smoke-verify the bundle manually: `bin/werkator --help`, `init` in a scratch repo, `config:print --full`, a short `server` run, and `init --systemd` unit content pointing into the bundle; document the results in this file.
- Verify on vm4006 (which has no Java): copy the bundle, run `bin/werkator --help` and `config:print`; document the results in this file.
## Acceptance Criteria
- `./gradlew ktlintFormat` then `./gradlew build` is green, with unchanged toolchain requirements.
- `./gradlew runtimeBundle` produces a tarball whose `bin/gittally` runs `--help`, `init`, and `server` on a machine without any Java runtime.
- `./gradlew runtimeBundle` produces a tarball whose `bin/werkator` runs `--help`, `init`, and `server` on a machine without any Java runtime.
- A fresh deployment to vm4006 following `docs/deployment.md` and `docs/migration-from-legacy.md` reaches a running service: web UI reachable, a Docker build succeeds, commit status arrives in Gitea, managed nginx/TLS works (`server.nginx.enabled: true`, DNS for `serverName` pointing at vm4006).
- vm2176 (legacy) keeps running in parallel during the migration; the legacy service is only retired after vm4006 is verified.
- Docs and ADR 0006 written as described; document deviations in this file.
@@ -71,15 +71,15 @@ Documentation:
Implemented as designed; no production-code change was needed.
The step was originally drafted for a GraalVM native image; it was re-planned to the jlink bundle after the Spring-AOT build-time condition evaluation turned out to be incompatible with the dual-context CLI/server wiring (see ADR 0006).
- `runtimeBundle` task in `build.gradle.kts` with the pinned module list (jdeps output plus java.logging, jdk.crypto.ec, jdk.management, jdk.zipfs); launcher script in `packaging/gittally`; tarball ~66 MB.
- `runtimeBundle` task in `build.gradle.kts` with the pinned module list (jdeps output plus java.logging, jdk.crypto.ec, jdk.management, jdk.zipfs); launcher script in `packaging/werkator`; tarball ~66 MB.
- Smoke tests on the dev machine (with `JAVA_HOME` unset and a stripped `PATH`): `--help`, `init --systemd` in a scratch repo, `config:print --full`, and a `server` run all passed; `/` served HTTP 200 and `/api/branches` returned JSON.
- The `init --systemd` unit generated from the bundle points at `<bundle>/jre/bin/java` and `<bundle>/lib/gittally.jar` as predicted — no detection code needed.
- Verified on vm4006 (no Java installed): bundle unpacked to `~/opt/gittally`, `--version`, `--help`, and `status` in a scratch repo (host git via `GitCommandRunner`) all worked.
- The `init --systemd` unit generated from the bundle points at `<bundle>/jre/bin/java` and `<bundle>/lib/werkator.jar` as predicted — no detection code needed.
- Verified on vm4006 (no Java installed): bundle unpacked to `~/opt/werkator`, `--version`, `--help`, and `status` in a scratch repo (host git via `GitCommandRunner`) all worked.
Production deployment to vm4006 (2026-08-10, same session):
- `hs.hsadmin.ng` cloned to `~/hs.hsadmin.ng` on vm4006; legacy configuration from vm2176 (repo `.gitTally` + `gitTally.env`) migrated to `.gittally.yml` per `docs/migration-from-legacy.md`; Gitea token moved (the token in vm2176's `gitTally.env` file was stale — the valid one came from the running legacy process environment).
- `statusContext: GitTally@vm4006` for the parallel phase; rename to `GitTally` after vm2176 is retired.
- `hs.hsadmin.ng` cloned to `~/hs.hsadmin.ng` on vm4006; legacy configuration from vm2176 (repo `.werkator` + `werkator.env`) migrated to `.werkator.yml` per `docs/migration-from-legacy.md`; Gitea token moved (the token in vm2176's `werkator.env` file was stale — the valid one came from the running legacy process environment).
- `statusContext: werkator@vm4006` for the parallel phase; rename to `werkator` after vm2176 is retired.
- systemd user service installed via `init --systemd` from the bundle and running; watcher fetches origin branches with the migrated credentials.
- Managed nginx/TLS live: Let's Encrypt certificate for `vm4006.hostsharing.net` obtained, `https://vm4006.hostsharing.net/` serves the UI with a valid chain, HTTP 301s to HTTPS (Hostsharing routes public 80/443 to `httpPort`/`httpsPort`, same as on vm2176).
- Fix discovered during rollout: certbot removed `ssl-dhparams.pem` from its repository, so the first nginx start failed with HTTP 404.
@@ -99,83 +99,89 @@ Fix: `HSADMINNG_POSTGRES_ADMIN_USERNAME=admin` and `HSADMINNG_POSTGRES_RESTRICTE
Verified by running both test classes in the build container with the variables set: green.
Open oddity: the same commit passed on vm2176 although neither its daemon environment, build image, Gradle volume, nor any build-script mechanism supplies these variables there (an unused git-ignored `.environment` file exists in its primary checkout, but nothing in the build reads it); the loading path on vm2176 remains unidentified.
Fourth finding (GitTally limitation, worked around in config): with all tests green, the build then failed in hsadmin-ng's `:prQuickCheck` — "fatal: not a git repository".
GitTally builds in a git worktree whose `.git` is a pointer file into the primary repository's `.git/worktrees/…`, and the Docker build container (deliberately, credentials live under `.git/gittally/`) only mounts the worktree — so build steps that call git fail; the legacy script avoided this by building in the primary checkout.
Fourth finding (Werkator limitation, worked around in config): with all tests green, the build then failed in hsadmin-ng's `:prQuickCheck` — "fatal: not a git repository".
Werkator builds in a git worktree whose `.git` is a pointer file into the primary repository's `.git/worktrees/…`, and the Docker build container (deliberately, credentials live under `.git/werkator/`) only mounts the worktree — so build steps that call git fail; the legacy script avoided this by building in the primary checkout.
Workaround: `prQuickCheck` removed from the vm4006 build command — it is a PR quality gate against a base branch and has no meaning in a post-merge master build (on vm2176 it only passed as an accidental no-op).
The underlying question (safe git availability inside Docker build containers without exposing `.git/gittally/` secrets) is left as a follow-up design task.
The underlying question (safe git availability inside Docker build containers without exposing `.git/werkator/` secrets) is left as a follow-up design task.
Cutover completed (2026-08-10, same day): after three green master builds and verified Gitea statuses from vm4006, the legacy service on vm2176 was disabled and removed from systemd.
vm4006's `statusContext` was switched to the canonical `GitTally` (effective without a restart — the Gitea client loads the config per call), and the branches still carrying red statuses from the buggy first hours were re-queued.
vm2176 now runs only a redirect nginx container (`gittally-redirect`, ports 8080/8443 like before): HTTP and HTTPS answer 301 to `https://vm4006.hostsharing.net$request_uri`, the ACME webroot keeps serving so the `nginx-letsencrypt-renew.timer` continues to renew the old host's certificate (the renew unit gained an `ExecStartPost` nginx reload).
GitTally answers the legacy static page names (`/index.html`, `/branches.html`, `/history.html`, `/system.html`, `/about.html`, `/license.html`) with permanent redirects to the new routes, so pre-rewrite links survive the host redirect.
vm4006's `statusContext` was switched to the canonical `werkator` (effective without a restart — the Gitea client loads the config per call), and the branches still carrying red statuses from the buggy first hours were re-queued.
vm2176 now runs only a redirect nginx container (`werkator-redirect`, ports 8080/8443 like before): HTTP and HTTPS answer 301 to `https://vm4006.hostsharing.net$request_uri`, the ACME webroot keeps serving so the `nginx-letsencrypt-renew.timer` continues to renew the old host's certificate (the renew unit gained an `ExecStartPost` nginx reload).
Werkator answers the legacy static page names (`/index.html`, `/branches.html`, `/history.html`, `/system.html`, `/about.html`, `/license.html`) with permanent redirects to the new routes, so pre-rewrite links survive the host redirect.
Update to v0.9.8 (2026-08-10): the running build was awaited first (a restart would have killed it), then service stopped, `~/opt/gittally` backed up to `~/opt/gittally.v0.9.7.bak` and the new bundle unpacked over it, service started.
Update to v0.9.8 (2026-08-10): the running build was awaited first (a restart would have killed it), then service stopped, `~/opt/werkator` backed up to `~/opt/werkator.v0.9.7.bak` and the new bundle unpacked over it, service started.
Verified live: `/` reports v0.9.8, the nav has no `Current` entry, the permanent `🔗` link appears only on branches whose latest build is their latest green one, and the newly linked reports answer 200 — including the stable `/branches/<branch>/reports/profile/`.
Master has no profile report yet because its `.gittally.yml` still carries the pre-PR#282 build command; it appears once that PR merges.
Master has no profile report yet because its `.werkator.yml` still carries the pre-PR#282 build command; it appears once that PR merges.
Update to v0.9.9 (2026-08-11): no build was running, service stopped, `~/opt/gittally` backed up to `~/opt/gittally.v0.9.8.bak` and replaced by the new bundle, service started.
The changed `server.bindAddress` default was harmless here because vm4006 sets `0.0.0.0` explicitly in `.git/gittally/.gittally.yml` — which the managed nginx container needs.
Found and fixed on the host: `.git/gittally/.gittally.yml` (the Gitea token) was still `0644` and its directory `0755` from the pre-0.9.9 `init`; both were tightened to `0600`/`0700` manually, as the new code only sets the mode for files it creates.
Update to v0.9.9 (2026-08-11): no build was running, service stopped, `~/opt/werkator` backed up to `~/opt/werkator.v0.9.8.bak` and replaced by the new bundle, service started.
The changed `server.bindAddress` default was harmless here because vm4006 sets `0.0.0.0` explicitly in `.git/werkator/.werkator.yml` — which the managed nginx container needs.
Found and fixed on the host: `.git/werkator/.werkator.yml` (the Gitea token) was still `0644` and its directory `0755` from the pre-0.9.9 `init`; both were tightened to `0600`/`0700` manually, as the new code only sets the mode for files it creates.
Verified live: `/` reports v0.9.9, HTTP 301s to HTTPS with a valid certificate, `/api/builds/latest` answers, a control token in the query string is rejected with 403, `config:print` masks `git.token`, and the mobile header stacks title over repository name.
Update to v0.9.10 (2026-08-11): same procedure, `~/opt/gittally.v0.9.9.bak` as the rollback copy.
Verified live: `/` reports v0.9.10, the pages no longer contain `gittally-control-token`, `/api/builds/latest` and `/branches` still answer 200 without any credential, and a mutation without the token is rejected with 403.
The operator has to paste the token from `~/hs.hsadmin.ng/.git/gittally/control-token` once per browser now.
Update to v0.9.10 (2026-08-11): same procedure, `~/opt/werkator.v0.9.9.bak` as the rollback copy.
Verified live: `/` reports v0.9.10, the pages no longer contain `werkator-control-token`, `/api/builds/latest` and `/branches` still answer 200 without any credential, and a mutation without the token is rejected with 403.
The operator has to paste the token from `~/hs.hsadmin.ng/.git/werkator/control-token` once per browser now.
Update to v0.9.11 (2026-08-14): same procedure, no build was running, `~/opt/gittally.0.9.10.bak` as the rollback copy.
Verified live: `bin/gittally --version` reports v0.9.11 before the start, the service is `active`, `/` answers 200 with v0.9.11 in the footer, `/releases` shows the v0.9.11 entry, and `/api/builds/current` is empty.
Update to v0.9.11 (2026-08-14): same procedure, no build was running, `~/opt/werkator.0.9.10.bak` as the rollback copy.
Verified live: `bin/werkator --version` reports v0.9.11 before the start, the service is `active`, `/` answers 200 with v0.9.11 in the footer, `/releases` shows the v0.9.11 entry, and `/api/builds/current` is empty.
The watcher's new local-ref fast-forward logged nothing, because `~/hs.hsadmin.ng` had already been reset to `origin/master` by hand — it only acts on a branch that actually lags behind.
Update to v0.9.12 (2026-08-26): same procedure, `~/opt/gittally.0.9.11.bak` as the rollback copy; a build was running — interrupted and re-enqueued by the startup recovery as designed.
Verified live: `bin/gittally --version` reports v0.9.12 before the start, the service is `active`, `/` shows v0.9.12, and the recovery re-enqueued exactly one build per affected branch (the pre-fix duplicate queue entries were collapsed by `markStaleRunningAsInterrupted` + `latestPerBranch`).
Update to v0.9.12 (2026-08-26): same procedure, `~/opt/werkator.0.9.11.bak` as the rollback copy; a build was running — interrupted and re-enqueued by the startup recovery as designed.
Verified live: `bin/werkator --version` reports v0.9.12 before the start, the service is `active`, `/` shows v0.9.12, and the recovery re-enqueued exactly one build per affected branch (the pre-fix duplicate queue entries were collapsed by `markStaleRunningAsInterrupted` + `latestPerBranch`).
Shipped fixes: prune never removes queued/running results (a branch deleted mid-build stays visible), and manual triggers dedup against an already active build of the same branch and commit.
Update to v0.9.13 (2026-08-28): same procedure, no build was running, `~/opt/gittally.0.9.12.bak` as the rollback copy.
Verified live: `bin/gittally --version` reports v0.9.13 before the start, the service is `active`, `/` answers 200 with v0.9.13 in the footer, `/api/builds/latest` carries the new `name` field, and the watcher polls without errors.
Update to v0.9.13 (2026-08-28): same procedure, no build was running, `~/opt/werkator.0.9.12.bak` as the rollback copy.
Verified live: `bin/werkator --version` reports v0.9.13 before the start, the service is `active`, `/` answers 200 with v0.9.13 in the footer, `/api/builds/latest` carries the new `name` field, and the watcher polls without errors.
Shipped feature: an `autoBuild.times` entry can carry its own `buildCommand` and `name` — a named nightly slot (e.g. `master@nightly`) gets its own branches-view row, retention pool, and permanent latest-green link; restart/retry/recovery repeat a build with its original command and name.
Update to v0.9.14 (2026-08-28): same procedure, `~/opt/gittally.0.9.13.bak` as the rollback copy; a build was running — interrupted and re-enqueued by the startup recovery as designed (now under its recorded build definition `default`).
Verified live: `bin/gittally --version` reports v0.9.14 before the start, the service is `active`, `/` answers 200 with v0.9.14 in the footer, the watcher polls without errors, and the deprecation warning for `branches.master.autoBuild` appears once in the log.
Update to v0.9.14 (2026-08-28): same procedure, `~/opt/werkator.0.9.13.bak` as the rollback copy; a build was running — interrupted and re-enqueued by the startup recovery as designed (now under its recorded build definition `default`).
Verified live: `bin/werkator --version` reports v0.9.14 before the start, the service is `active`, `/` answers 200 with v0.9.14 in the footer, the watcher polls without errors, and the deprecation warning for `branches.master.autoBuild` appears once in the log.
Shipped feature: named build definitions (ADR 0007) — the `builds` section defines jobs with `onPush`/`atTimes` triggers, branch selectors (globs, `activeWithin`), and setting overrides; named builds record under `<branch>@<build>` pools; the v0.9.13 per-slot syntax was removed again.
Update to v0.9.15 (2026-08-29): same procedure, `~/opt/gittally.0.9.14.bak` as the rollback copy, no build was running.
Verified live: `bin/gittally --version` reports v0.9.15 before the start, the service is `active`, the API answers, and the watcher polls without errors.
Update to v0.9.15 (2026-08-29): same procedure, `~/opt/werkator.0.9.14.bak` as the rollback copy, no build was running.
Verified live: `bin/werkator --version` reports v0.9.15 before the start, the service is `active`, the API answers, and the watcher polls without errors.
The leftover `builds.maxConcurrent` in the repository's committed `master` config is ignored with exactly one warning per context instead of failing the configuration — the reason it is tolerated rather than rejected: that config needs a colleague's approval to change, and the installation must not be stuck on a key it is meant to forget.
Shipped feature: the `.gittally.yml` committed on a branch takes precedence for its `builds` section as well, and the watcher reads it per origin branch (`git show`, cached by head commit) to decide which of that branch's builds are due.
Shipped feature: the `.werkator.yml` committed on a branch takes precedence for its `builds` section as well, and the watcher reads it per origin branch (`git show`, cached by head commit) to decide which of that branch's builds are due.
Verified live within seconds of the start: the build definition `reactivate-pi-test-with-full-pitest`, which exists only on the branch `mihoe/reactivate-pi-test` and not in the `master` config, fired its due 05:00 UTC slot, recorded under the pool `mihoe/reactivate-pi-test@reactivate-pi-test-with-full-pitest`, and ran the branch's `pitestFull` command in the build container inherited from the `master` config.
Update to v0.9.16 (2026-08-29): same procedure, `~/opt/gittally.0.9.15.bak` as the rollback copy.
Update to v0.9.16 (2026-08-29): same procedure, `~/opt/werkator.0.9.15.bak` as the rollback copy.
A build was running at the first attempt, so the deployment aborted itself before stopping anything; the service was then stopped in the first idle window (waiting rather than interrupting was the operator's call).
Verified live: `bin/gittally --version` reports v0.9.16 before the start, the service is `active`, `/releases` lists v0.9.16, and the watcher polls without errors.
Verified live: `bin/werkator --version` reports v0.9.16 before the start, the service is `active`, `/releases` lists v0.9.16, and the watcher polls without errors.
Shipped features: hourly scheduled builds (`atTimes: ["??:05"]`) and the artifact page showing the command a build actually runs.
Verified live: the branch's `atTimes: ["??:00"]` — rejected by v0.9.15 with a warning on every poll cycle — is accepted, and its 06:00 UTC slot fired right after the restart.
Update to v0.9.17 (2026-08-29): same procedure, `~/opt/gittally.0.9.16.bak` as the rollback copy.
Update to v0.9.17 (2026-08-29): same procedure, `~/opt/werkator.0.9.16.bak` as the rollback copy.
Deployed under the operator's condition "only if no build is running": the deploy script re-checks `/api/builds/current` immediately before the stop and exits without touching anything when a build is executing.
Verified live: `bin/gittally --version` reports v0.9.17 before the start, the service is `active`, `/releases` lists v0.9.17, the served `gittally.js` carries the resume listeners, and no errors in the log.
Verified live: `bin/werkator --version` reports v0.9.17 before the start, the service is `active`, `/releases` lists v0.9.17, the served `werkator.js` carries the resume listeners, and no errors in the log.
Shipped fix: a page returning from the background fetches the current state immediately instead of showing (and ticking) the state it was left in.
Update to v0.9.18 (2026-08-29): same procedure, `~/opt/gittally.0.9.17.bak` as the rollback copy, no build was running.
Verified live: `bin/gittally --version` reports v0.9.18 before the start, the service is `active`, `/releases` lists v0.9.18, the watcher polls without fetch or poll errors, and the only warnings are the two known `builds.maxConcurrent` lines from the repository's committed config.
Shipped feature: a configuration file can declare the GitTally it is written for (`gitTally.version.since`/`below`), so an incompatibility is named instead of silently ignored.
Update to v0.9.18 (2026-08-29): same procedure, `~/opt/werkator.0.9.17.bak` as the rollback copy, no build was running.
Verified live: `bin/werkator --version` reports v0.9.18 before the start, the service is `active`, `/releases` lists v0.9.18, the watcher polls without fetch or poll errors, and the only warnings are the two known `builds.maxConcurrent` lines from the repository's committed config.
Shipped feature: a configuration file can declare the Werkator it is written for (`werkator.version.since`/`below`), so an incompatibility is named instead of silently ignored.
The configs of the watched repository declare nothing yet and are unaffected — a missing declaration is never an error.
Update to v0.9.19 (2026-08-29): same procedure, `~/opt/gittally.0.9.18.bak` as the rollback copy, no build was running.
Update to v0.9.19 (2026-08-29): same procedure, `~/opt/werkator.0.9.18.bak` as the rollback copy, no build was running.
Shipped feature: a build definition describes its build completely (`requirePullRequest`, the whole `docker` section), `builds.default` is the settings base of every other definition, and the per-branch `branches` section is superseded — read only while nothing defines a build at all.
The machine config `.git/gittally/.gittally.yml` was rewritten to the new shape beforehand (backup `.gittally.yml.20260829T085959Z.bak`), in a form valid under both versions: `builds.default` plus `builds.master` for the new one, a reduced `branches` block for v0.9.18, which has no sandbox policy inside a definition.
The machine config `.git/werkator/.werkator.yml` was rewritten to the new shape beforehand (backup `.werkator.yml.20260829T085959Z.bak`), in a form valid under both versions: `builds.default` plus `builds.master` for the new one, a reduced `branches` block for v0.9.18, which has no sandbox policy inside a definition.
It also drops the machine-local `--no-build-cache` command that had shadowed master's own `buildCommand` for every on-push build; the commands now come from `origin/master`'s committed config.
Verified live: `bin/gittally --version` reports v0.9.19 before the start, the service is `active`, `/releases` lists v0.9.19, and the log carries the expected "ignoring the branches section" warning next to the known `builds.maxConcurrent` one.
Verified live: `bin/werkator --version` reports v0.9.19 before the start, the service is `active`, `/releases` lists v0.9.19, and the log carries the expected "ignoring the branches section" warning next to the known `builds.maxConcurrent` one.
The new `master@master` job fired on the first poll after the restart — its 01:00 slot was due and unmarked for that pool — and runs in `hsadmin-ng-build-env:latest` with `bootJarWithDocumentation`: the pinned `docker.enabled`/`network` reached it through the inheritance from `builds.default`, while its own command won.
The legacy `branches` block stays in the machine config for the transition week as the rollback path to v0.9.18; it is inert under v0.9.19.
Update to v0.9.20 (2026-08-29): same procedure, `~/opt/gittally.0.9.19.bak` as the rollback copy, no build was running.
Update to v0.9.20 (2026-08-29): same procedure, `~/opt/werkator.0.9.19.bak` as the rollback copy, no build was running.
Shipped feature: a definition's `trigger` block, `!` exclusion patterns in `trigger.branches`, and a per-build Gitea `statusContext`.
The machine config had to be migrated in the same window (backup `.gittally.yml.20260829T100842Z.bak`): v0.9.19 drops an unknown `trigger` block silently — which would leave every definition without a trigger — and v0.9.20 refuses the flat keys, so the file is valid for exactly one of the two versions and had to be swapped while the service was down.
The machine config had to be migrated in the same window (backup `.werkator.yml.20260829T100842Z.bak`): v0.9.19 drops an unknown `trigger` block silently — which would leave every definition without a trigger — and v0.9.20 refuses the flat keys, so the file is valid for exactly one of the two versions and had to be swapped while the service was down.
The new configuration was validated with the new binary (`config:print --full`) after the swap and before the start.
Verified live: `--version` reports v0.9.20, the service is `active`, `/releases` lists v0.9.20, the watcher polls without errors, and the two expected warnings (`builds.maxConcurrent`, the ignored `branches` section) are the only ones from the host configuration.
The branch-scoped refusal showed itself in production immediately: `mihoe/reactivate-pi-test` still commits its triggers flat, so its committed config is refused with a message naming the branch, the commit, and each definition's offending keys — the watcher falls back to the host's definitions for scheduling, and builds of that branch fail until the file is migrated. Every other branch is unaffected, which is the whole point of the per-file scoping.
Update to v0.9.21 (2026-08-30): same procedure, `~/opt/gittally.0.9.20.bak` as the rollback copy, no build was running; the machine config needed no change this time.
Update to v1.0.0 (2026-08-31), the rename to Werkator, deployed from the branch as the final test of PR#1: `~/opt/gittally.0.9.21.bak` as the rollback copy, no build was running, and an 8.8 KB snapshot of the state directory without its worktrees (`~/gittally-state-20260831T065716Z.tar.gz`) as the way back — everything that cannot be recreated fits in it, the 878 MB are worktrees the migration drops and the next build recreates.
The artifact root was moved by hand (`~/.local/state/gittally` to `~/.local/state/werkator`, 1.3 GB, instant on the same filesystem), the state directory moved itself.
Two defects surfaced and were fixed before the service was started, both of the silent kind this release is about: the directory move left the machine configuration under its old name in the new directory, a pair of names the lookup did not expect, so the instance resolved empty credentials and none of the host's build definitions; and `init --systemd` wrote a fresh template `.werkator.yml` into the watched working tree beside its committed `.gittally.yml`, which would have made that repository build `./gradlew test` instead of its own command. The bundle was rebuilt from the fixed branch before the units were switched.
Verified live: `--version` reports v1.0.0, the service is `active`, the build history is the one from before, the watcher polls without errors, HTTPS through the managed nginx answers 200 with the certificates found under the moved state path, and a master build started by itself — worktree recreated, container `werkator-build-…` running in `hsadmin-ng-build-env:latest` under the new `org.hoennig.werkator` label.
The two expected warnings (`builds.maxConcurrent`, the ignored `branches` section) are still the only ones, both from master's committed configuration.
Update to v0.9.21 (2026-08-30): same procedure, `~/opt/werkator.0.9.20.bak` as the rollback copy, no build was running; the machine config needed no change this time.
Shipped feature: an unreachable origin is shown in the web UI (step 19), and a lasting fetch failure is logged once per message instead of once per poll.
The occasion was an outage the same morning: the `git.token` in the machine config had been overwritten with a placeholder string, GitTally failed every fetch for 57 minutes, and the branches view kept showing its last known list as if nothing were wrong.
Verified live: `--version` reports v0.9.21, the service is `active`, `/` answers 200 with v0.9.21 in the footer, `/api/watcher` reports `lastFetchError: null`, the served `gittally.js` carries `refreshWatcherBanner`, `/branches` carries the banner element, and the only warnings are the two expected ones from the repository's committed config (`builds.maxConcurrent`, the ignored `branches` section).
The occasion was an outage the same morning: the `git.token` in the machine config had been overwritten with a placeholder string, Werkator failed every fetch for 57 minutes, and the branches view kept showing its last known list as if nothing were wrong.
Verified live: `--version` reports v0.9.21, the service is `active`, `/` answers 200 with v0.9.21 in the footer, `/api/watcher` reports `lastFetchError: null`, the served `werkator.js` carries `refreshWatcherBanner`, `/branches` carries the banner element, and the only warnings are the two expected ones from the repository's committed config (`builds.maxConcurrent`, the ignored `branches` section).
+7 -7
View File
@@ -6,14 +6,14 @@ Motivated by the vm4006 rollout (step 15, fourth finding): hs.hsadmin.ng's build
## Problem
Builds run in git worktrees under `.git/gittally/worktrees/<branchKey>`.
Builds run in git worktrees under `.git/werkator/worktrees/<branchKey>`.
A worktree's `.git` is a pointer file into the primary repository's `.git/worktrees/<key>`, and `DockerBuildRunner` bind-mounts only the worktree — so every git call inside the build container fails.
The legacy script did not have this problem because it built in the primary checkout with the real `.git` present (read-write, including all secrets stored next to it — full exposure).
Hard invariant to preserve: a branch build must never be able to reach credentials; `.git/gittally/.gittally.yml` (`git.token`) and the control token live under `.git`.
Hard invariant to preserve: a branch build must never be able to reach credentials; `.git/werkator/.werkator.yml` (`git.token`) and the control token live under `.git`.
## Considered Options
- **Read-only `.git` mount with `.git/gittally/` masked (chosen)** — three layered mounts, no config key, no workspace mutation; strictly less privileged than legacy.
- **Read-only `.git` mount with `.git/werkator/` masked (chosen)** — three layered mounts, no config key, no workspace mutation; strictly less privileged than legacy.
- Copy minimal git metadata into the workspace (admin dir plus `objects/info/alternates`) — mutates the workspace, still needs the object database mounted, more moving parts.
- Document the limitation and require git-free build commands — pushes the problem onto every watched project; hsadmin-ng shows real builds do call git.
@@ -22,11 +22,11 @@ Hard invariant to preserve: a branch build must never be able to reach credentia
`DockerBuildRunner.gitMetadataMounts(workspace, repoDir)` adds three mounts when (and only when) the workspace is a worktree of `repoDir` (detected via the `gitdir:` pointer file, which must resolve into `repoDir/.git`):
1. `repoDir/.git` → same path, **read-only**: objects, refs, and the worktree admin metadata become resolvable; object and ref writes stay impossible.
2. An empty **tmpfs over `repoDir/.git/gittally`**: masks the machine config (`git.token`), the control token, and all GitTally state; the workspace bind (deeper path, Docker nests mounts by target depth) resurfaces only this build's own worktree inside the masked directory.
2. An empty **tmpfs over `repoDir/.git/werkator`**: masks the machine config (`git.token`), the control token, and all Werkator state; the workspace bind (deeper path, Docker nests mounts by target depth) resurfaces only this build's own worktree inside the masked directory.
3. `repoDir/.git/worktrees/<key>` → same path, **read-write**: the worktree's admin dir (HEAD, index), so index-refreshing commands like `git status` work.
No configuration key: the exposure is strictly smaller than the legacy baseline, and a knob would join the pinned sandbox-policy set without a known use case.
Remaining, documented exposure: the rest of `.git` — including `.git/config` — is readable by builds; GitTally never stores credentials there (fetch auth uses a secret-free `GIT_ASKPASS` with env-passed credentials).
Remaining, documented exposure: the rest of `.git` — including `.git/config` — is readable by builds; Werkator never stores credentials there (fetch auth uses a secret-free `GIT_ASKPASS` with env-passed credentials).
## Tests
@@ -35,10 +35,10 @@ Remaining, documented exposure: the rest of `.git` — including `.git/config`
## Acceptance Criteria
- `./gradlew ktlintFormat` then `./gradlew build` is green.
- In a real Docker build worktree: `git log`/`git status` succeed inside the container, `.git/gittally/.gittally.yml` and `control-token` are not readable, and a `git push`/ref write fails.
- In a real Docker build worktree: `git log`/`git status` succeed inside the container, `.git/werkator/.werkator.yml` and `control-token` are not readable, and a `git push`/ref write fails.
- `docs/configuration.md` (docker notes) and the architecture skill describe the mounts.
## Result (2026-08-10)
Implemented as designed; verified on vm4006 (see below) and in unit tests.
`sh -c 'git log -1 && git status --short && cat .../.git/gittally/.gittally.yml'` inside a build container of the hs.hsadmin.ng worktree: git commands succeed, the machine config read fails with "No such file or directory", `git update-ref` fails on the read-only filesystem.
`sh -c 'git log -1 && git status --short && cat .../.git/werkator/.werkator.yml'` inside a build container of the hs.hsadmin.ng worktree: git commands succeed, the machine config read fails with "No such file or directory", `git update-ref` fails on the read-only filesystem.
+17 -17
View File
@@ -1,14 +1,14 @@
# Step 17: Running GitTally on a Managed Webspace (bubblewrap builds + web access)
# Step 17: Running Werkator on a Managed Webspace (bubblewrap builds + web access)
Prerequisites: steps 11, 15, 16.
Read `README.md` first.
Motivated by running GitTally on Hostsharing **Managed Webspaces**: no root, no Docker daemon, but `bwrap` (bubblewrap) is available and unprivileged user namespaces are allowed.
Target use case: GitTally builds GitTally itself on a Managed Webspace; builds needing special dependencies get them from a prepared root filesystem instead of the host.
Motivated by running Werkator on Hostsharing **Managed Webspaces**: no root, no Docker daemon, but `bwrap` (bubblewrap) is available and unprivileged user namespaces are allowed.
Target use case: Werkator builds Werkator itself on a Managed Webspace; builds needing special dependencies get them from a prepared root filesystem instead of the host.
Projects that need Docker for their own tests (hs.hsadmin.ng with Testcontainers) stay on a container host like vm4006 — the webspace is for Docker-free builds only.
The step covers two halves of the same deployment and is deliberately not split:
the build sandbox (most of this document) and the web access under a domain (last section).
Without the second half the first one only proves that sandboxed builds work somewhere; without the first one GitTally on a webspace would run builds unsandboxed on the host.
Without the second half the first one only proves that sandboxed builds work somewhere; without the first one Werkator on a webspace would run builds unsandboxed on the host.
## Precondition Check (run on the target webspace first)
@@ -46,7 +46,7 @@ What 0.8.0 lacks is overlayfs (`--overlay`, added in 0.9.0): a future "throwaway
**The runtime bundle runs there — checked, not assumed.** The webspace has glibc 2.36 (Debian 12), below the dev machine's 2.39, which by ADR 0006's original wording would have ruled the bundle out.
That wording was wrong and has been corrected: the bundle's highest required symbol version is `GLIBC_2.15`, because `jlink` copies Temurin's prebuilt binaries rather than compiling anything.
So no container build and no second build machine are needed for this platform.
The bundle's `java.desktop` module does carry X11, ALSA and freetype dependencies, but only in the AWT libraries, which a headless GitTally never loads — as on vm4006.
The bundle's `java.desktop` module does carry X11, ALSA and freetype dependencies, but only in the AWT libraries, which a headless Werkator never loads — as on vm4006.
## Goal
@@ -58,8 +58,8 @@ No root on the host, no Docker daemon, no changes to the native and Docker runti
### Prepared root filesystem
`debootstrap`/`mmdebstrap` are not available on the webspace, so the rootfs is **not created on the target system**.
It is built once elsewhere (any machine with Docker or root, e.g. a container VM) and distributed as an archive, e.g. `gittally-buildenv-trixie-java21.tar.zst`, containing Debian plus all build dependencies (JDK 21, git, locales, project-specific tools).
GitTally unpacks it on demand (`tar --no-same-owner`) into `.git/gittally/buildenv/<envKey>/rootfs`**not** into the working tree.
It is built once elsewhere (any machine with Docker or root, e.g. a container VM) and distributed as an archive, e.g. `werkator-buildenv-trixie-java21.tar.zst`, containing Debian plus all build dependencies (JDK 21, git, locales, project-specific tools).
Werkator unpacks it on demand (`tar --no-same-owner`) into `.git/werkator/buildenv/<envKey>/rootfs`**not** into the working tree.
Like the Docker image and the Gradle cache volume, the environment is shared across all branch worktrees and survives worktree pruning; `<envKey>` derives from a hash of the configured archive source, so an environment-version change unpacks a fresh rootfs and stale ones can be pruned.
### Configuration
@@ -67,7 +67,7 @@ Like the Docker image and the Gradle cache volume, the environment is shared acr
New `branches.<name>.bwrap` section: `enabled`, `rootfs` (path or URL of the archive), `env` (like `docker.env`).
`bwrap.enabled` and `bwrap.rootfs` join the **pinned sandbox-policy set** (like `docker.enabled`/`docker.network`): a branch must not be able to switch off its sandbox or substitute a foreign rootfs via its committed config.
`docker.enabled` and `bwrap.enabled` are mutually exclusive per branch — reject the config, do not pick silently.
Keep the three config places in sync: `GitTallyConfig`, the `InitCommand` templates, `docs/configuration.md`.
Keep the three config places in sync: `WerkatorConfig`, the `InitCommand` templates, `docs/configuration.md`.
### Invocation
@@ -84,7 +84,7 @@ bwrap --unshare-user --unshare-pid --die-with-parent --uid 0 --gid 0 \
/bin/sh -c '<buildCommand>'
```
- The workspace is bound at its **host path**, not at `/workspace`: the worktree's `.git` pointer file contains absolute host paths, and the step-16 git metadata mounts (`--ro-bind` of the primary `.git`, `--tmpfs` over `.git/gittally`, `--bind` of `.git/worktrees/<key>`) port 1:1 — reuse that logic, do not duplicate it.
- The workspace is bound at its **host path**, not at `/workspace`: the worktree's `.git` pointer file contains absolute host paths, and the step-16 git metadata mounts (`--ro-bind` of the primary `.git`, `--tmpfs` over `.git/werkator`, `--bind` of `.git/worktrees/<key>`) port 1:1 — reuse that logic, do not duplicate it.
- `<buildenv>/home` bound as `/root` gives Gradle a persistent `$HOME` (wrapper dists, `.gradle` caches) — the bwrap sibling of the Docker runner's Gradle cache volume.
- `--die-with-parent` plus `--unshare-pid`: cancellation kills the returned `bwrap` process tree and nothing survives — same semantics as the other runtimes.
- No ownership repair is needed: files created as uid 0 inside the namespace are owned by the webspace user on the host.
@@ -93,7 +93,7 @@ bwrap --unshare-user --unshare-pid --die-with-parent --uid 0 --gid 0 \
- Network stays shared with the host (Gradle needs it); isolation is weaker than Docker's per-container network.
- No Docker inside the sandbox, so no Testcontainers-based tests; build commands must select a Docker-free test subset.
For GitTally's own build this means `TestcontainersSmokeTest` must become conditional (`enabledIf` docker present) — that change is part of this step.
For Werkator's own build this means `TestcontainersSmokeTest` must become conditional (`enabledIf` docker present) — that change is part of this step.
## Web Access under a Domain (no Docker, no managed nginx)
@@ -103,19 +103,19 @@ Three platform-side prerequisites, none of them code:
1. **Book the "eigener Serverdienst" option** — a service user plus one reserved localhost port, requested from `service@hostsharing.net` stating the service user and the number of ports.
Surcharged on Managed Webspaces (RAM contingent in 128 MB steps), included on Managed Servers.
The port number is **assigned by Hostsharing** (wiki examples use 34567, 38005/38006), so it goes into `server.port`GitTally's 18080 is not available by choice.
The port number is **assigned by Hostsharing** (wiki examples use 34567, 38005/38006), so it goes into `server.port`Werkator's 18080 is not available by choice.
Sources: [Individuelle Serverdienste](https://www.hostsharing.net/features/individuelle-serverdienste/), [Apache](https://www.hostsharing.net/features/apache/).
2. **Run the service as a systemd user unit** — mandatory on Managed Webspaces (no `nohup`, no supervisord); lingering needs a valid login shell configured in HSAdmin, and the account's RAM is capped by a slice (`systemctl status pacs-<account>.slice`).
`gittally init --systemd` already generates the unit and the `gittally.env`, whose `JAVA_OPTS=-Xmx…` is what keeps the JVM inside the slice.
`werkator init --systemd` already generates the unit and the `werkator.env`, whose `JAVA_OPTS=-Xmx…` is what keeps the JVM inside the slice.
Source: [Prozessmanagement mit systemd im Userspace](https://wiki.hostsharing.net/index.php/Prozessmanagement_mit_systemd_im_Userspace).
3. **Let's Encrypt** is a domain option ticked in HSAdmin (free, automatic, includes the wildcard subdomain; requires the domain's nameservers to be delegated to Hostsharing), so TLS terminates in the managed Apache.
Source: [TLS](https://www.hostsharing.net/doc/managed-operations-platform/tls/).
### User model: a dedicated unix user, not the package admin
GitTally runs as its own unix user, e.g. `xyz00-gittally`, with the domain assigned to that same user (`domain.add({set:{name:'…',user:'xyz00-gittally'}})`), so the service, its repository checkout and `~/doms/<domain>/htdocs-ssl/` share one home directory.
Werkator runs as its own unix user, e.g. `xyz00-werkator`, with the domain assigned to that same user (`domain.add({set:{name:'…',user:'xyz00-werkator'}})`), so the service, its repository checkout and `~/doms/<domain>/htdocs-ssl/` share one home directory.
That is what every Hostsharing service guide does (`xyz00-chat` for Mattermost, `xyz00-tomcat`, `xyz00-cloud` for Nextcloud) and what their user documentation recommends: a domain *can* run under the package admin, but "aus Sicherheitsgründen empfiehlt es sich aber Domains auf separate Domain-Admins aufzuschalten", so a compromise stays inside one home instead of reaching the whole package.
Here the argument is stronger than usual, because GitTally checks out foreign commits and executes their build scripts — running that as the package admin would undo the sandbox rationale of this very step.
Here the argument is stronger than usual, because Werkator checks out foreign commits and executes their build scripts — running that as the package admin would undo the sandbox rationale of this very step.
The service user is named when ordering the daemon port anyway.
Sources: [Benutzer](https://www.hostsharing.net/doc/managed-operations-platform/benutzer/), [HSAdmin domain](https://www.hostsharing.net/doc/managed-operations-platform/hsadmin/domain/).
@@ -137,7 +137,7 @@ RewriteRule .* http://127.0.0.1:<assigned-port>%{REQUEST_URI} [proxy]
Sources: [Mattermost Installieren](https://wiki.hostsharing.net/index.php/Mattermost_Installieren), [Tomcat Installieren](https://wiki.hostsharing.net/index.php?title=Tomcat_Installieren).
The matching GitTally configuration:
The matching Werkator configuration:
```yaml
server:
@@ -148,7 +148,7 @@ server:
enabled: false # the managed nginx container is not used on a webspace
```
**This half needs no code change.** GitTally never reconstructs absolute URLs from the request — everything external comes from `server.publicBaseUrl` and the UI links relatively — so the usual reverse-proxy fix `server.forward-headers-strategy` is not needed.
**This half needs no code change.** Werkator never reconstructs absolute URLs from the request — everything external comes from `server.publicBaseUrl` and the UI links relatively — so the usual reverse-proxy fix `server.forward-headers-strategy` is not needed.
Two claims could **not** be verified from a Hostsharing primary source; check them on the target webspace rather than relying on them:
@@ -169,6 +169,6 @@ Write ADR 0007: bubblewrap user-namespace sandbox as the third build runtime (op
- The precondition command line above passes on the target webspace; its output is recorded in this file.
- `./gradlew ktlintFormat` then `./gradlew build` is green — also on a machine without Docker (Testcontainers smoke test skipped, not failed).
- On a Managed Webspace: GitTally (from the runtime bundle) builds a real branch of a repo inside the bwrap sandbox; git commands work in the worktree; `.git/gittally/` is not readable from the build; a write to `/usr` fails.
- On a Managed Webspace: Werkator (from the runtime bundle) builds a real branch of a repo inside the bwrap sandbox; git commands work in the worktree; `.git/werkator/` is not readable from the build; a write to `/usr` fails.
- On the same webspace: the UI answers over HTTPS under the domain through the Apache `.htaccess` proxy, the service survives a logout and a reboot (systemd lingering), and Gitea statuses carry `publicBaseUrl` links that resolve.
- Docs updated: `docs/configuration.md` (bwrap section), architecture skill (third runtime), ADR 0007, and `docs/deployment.md` gains "Hostsharing Managed Webspace" as a third deployment variant — written only once the setup above is verified on a real webspace, not from this plan.
+10 -11
View File
@@ -11,12 +11,12 @@ The `trigger` block, originally planned here, shipped earlier — see the sectio
## Precondition Check (run first, do not skip)
The removal adds a *rejection by name*: a configuration file that still carries a `branches:` key is refused, because a silently ignored section is exactly the failure this step exists to prevent — and the `gitTally.version` check cannot catch it, since it only bites files that declare a version, which none of the hs.hsadmin.ng configs do.
The removal adds a *rejection by name*: a configuration file that still carries a `branches:` key is refused, because a silently ignored section is exactly the failure this step exists to prevent — and the `werkator.version` check cannot catch it, since it only bites files that declare a version, which none of the hs.hsadmin.ng configs do.
So no configuration still in play may contain the section. On vm4006:
```bash
ssh tallyman@vm4006.hostsharing.net 'cd ~/hs.hsadmin.ng && grep -n "^branches:" .git/gittally/.gittally.yml; for b in $(git for-each-ref --format="%(refname:short)" refs/remotes/origin | sed "s|origin/||"); do if git cat-file -e origin/$b:.gittally.yml 2>/dev/null && git show origin/$b:.gittally.yml | grep -qE "^branches:"; then echo "still legacy: $b"; fi; done'
ssh tallyman@vm4006.hostsharing.net 'cd ~/hs.hsadmin.ng && grep -n "^branches:" .git/werkator/.werkator.yml; for b in $(git for-each-ref --format="%(refname:short)" refs/remotes/origin | sed "s|origin/||"); do if git cat-file -e origin/$b:.werkator.yml 2>/dev/null && git show origin/$b:.werkator.yml | grep -qE "^branches:"; then echo "still legacy: $b"; fi; done'
```
Expected output: nothing at all.
@@ -24,13 +24,13 @@ Expected output: nothing at all.
As of 2026-08-29 this listed `master`, five `mihoe/…` branches, and the machine config;
the machine config was cleaned up on 2026-08-30, so only the committed ones are left.
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.
Branches without a committed `.werkator.yml` are fine — they build from the machine config.
Note that `branches:` also exists as the *selector* key **inside** a build definition (`builds.<name>.branches: ["master"]`). That one stays. Only the top-level section goes. The grep above anchors at the line start for exactly that reason.
## Code
- `config/GitTallyConfig.kt`: drop the `branches` property and `AutoBuildConfig`, and `BranchConfig.autoBuild` with it.
- `config/WerkatorConfig.kt`: drop the `branches` property and `AutoBuildConfig`, and `BranchConfig.autoBuild` with it.
Rename `BranchConfig` to `BuildSettings` — with the section gone it is no longer a schema type but the resolved answer to "what does this build run", which is all it is used for.
`buildSettings(branch, build)` then no longer needs the branch lookup: `effectiveBuildDefinitions()[build]?.applyTo(BuildSettings()) ?: BuildSettings()`.
Keep the `branch` parameter — the callers pass it and a later per-branch concern would need it back.
@@ -39,9 +39,9 @@ Note that `branches:` also exists as the *selector* key **inside** a build defin
Reuse `ConfigVersionException` or add a sibling; the message must name the file and say where the settings belong now.
- `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.
This is the first real use of that mechanism: a file declaring `werkator.version.since` below this release is then refused with a message naming the change.
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.
- `commands/InitCommand.kt`: nothing — the generated template has been `builds`-only with a `trigger` block since v0.9.20. Verify with `werkator init` in a scratch repo.
## Already done: the `trigger` block
@@ -66,7 +66,6 @@ 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.
## Production
@@ -75,13 +74,13 @@ Its legacy `branches` block is deleted, `builds.master` is now `builds.nightly`,
The file holds `git`, `server`, `builds.default`, and `builds.nightly`; `config:print --full` resolves both definitions completely, with `nightly` inheriting the docker settings and all three artifact directories.
Nothing about the section removal itself is left to do there.
The file is 600 by design; a shell redirect creates 644, so check the mode after every edit.
Backups: `.gittally.yml.20260829T085959Z.bak` (the pre-v0.9.19 shape) and `.gittally.yml.20260829T100842Z.bak` (the last one carrying the legacy block).
Backups: `.werkator.yml.20260829T085959Z.bak` (the pre-v0.9.19 shape) and `.werkator.yml.20260829T100842Z.bak` (the last one carrying the legacy block).
What remains is deleting the `builds` section from the machine config entirely, as soon as master carries its own.
The file then holds `git` and `server` — the secrets and the host's addresses — and nothing that describes a build.
That works because the pinning strips the *branch* layer only (`ConfigLoader.stripPinned`, applied in `withBranchLayer`).
Master's committed `.gittally.yml` is the project layer of the primary checkout and is merged unstripped, so its `docker.enabled: true` and `network: host` are what every branch's builds inherit — including a build a branch invents for itself, because the inheritance from `builds.default` runs after the layers are merged.
Master's committed `.werkator.yml` is the project layer of the primary checkout and is merged unstripped, so its `docker.enabled: true` and `network: host` are what every branch's builds inherit — including a build a branch invents for itself, because the inheritance from `builds.default` runs after the layers are merged.
The sandbox policy thereby moves from the host to master, where changing it needs a review; it must therefore actually be in master's file before the host's section goes.
`builds.nightly` goes with it: master's config defines its own `release` job, and the nightly rebuild belongs next to it rather than on the host.
@@ -94,12 +93,12 @@ Deploy as usual (`docs/plan/15-runtime-bundle-distribution.md`), only while `/ap
## Verification
- `gittally config:print --full` on vm4006 before the restart: no `branches` in the output, every definition complete, and `docker.enabled: true` plus `network: host` on all of them.
- `werkator config:print --full` on vm4006 before the restart: no `branches` in the output, every definition complete, and `docker.enabled: true` plus `network: host` on all of them.
Once the machine config's `builds` section is gone, that resolves entirely from master's committed file — which is exactly what the check is for.
- 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 confirm the error names the file and the way out.
## Rollback
Keep the `~/opt/gittally.0.9.20.bak` that this step's deployment creates, plus a timestamped copy of the machine config.
Keep the `~/opt/werkator.0.9.20.bak` that this step's deployment creates, plus a timestamped copy of the machine config.
Going back below v0.9.19 is not provided for: v0.9.18 reads its sandbox policy from the `branches` block that no configuration carries any more, and would build natively on the host.
+3 -3
View File
@@ -6,7 +6,7 @@ Read `README.md` first.
## Why
On 2026-08-30 the Gitea token in the machine config on vm4006 was replaced by a placeholder string.
For 57 minutes GitTally failed `git fetch --prune origin` every ten seconds and wrote 297 warnings to the journal.
For 57 minutes Werkator failed `git fetch --prune origin` every ten seconds and wrote 297 warnings to the journal.
The branches view showed a calm, ordinary list the whole time: every branch with its last build, nothing amiss.
The failure was noticed only because an expected build did not start, and it took reading the journal to see why.
@@ -27,11 +27,11 @@ The endpoint needs no change.
## Code
- `static/gittally.js`: fetch `/api/watcher` from the same polling cycle that refreshes the view, and show a banner while any of the three conditions holds.
- `static/werkator.js`: fetch `/api/watcher` from the same polling cycle that refreshes the view, and show a banner while any of the three conditions holds.
Keep the existing discipline — a timeout on the fetch, and a failure of *this* request must never break the view's own refresh.
- `templates/fragments.html`: add a `watcher-banner` fragment to the `nav(view)` row so every view inherits it; hidden unless the script fills it.
- Wording says what is stale and since when, not just that something failed: the branch list is not updating, since `lastPollAt`, because `<error>`.
Timestamps go through the shared formatting — `UiFormats` and `gittally.js` must produce identical formats (invariant in `AGENTS.md`).
Timestamps go through the shared formatting — `UiFormats` and `werkator.js` must produce identical formats (invariant in `AGENTS.md`).
- Do not overload `live-indicator`: it reports whether the *browser* reaches the server.
This banner reports whether the *server* reaches origin. Two independent failures, two independent signals.
+10 -9
View File
@@ -1,6 +1,6 @@
# GitTally Rewrite Plan
# Werkator Rewrite Plan
This directory contains the step-by-step plan for rewriting `legacy/gitTally` (bash) as the Kotlin/Spring application in this repository.
This directory contains the step-by-step plan for rewriting the legacy bash script as the Kotlin/Spring application in this repository.
Each step file is self-contained and sized for one focused Claude Code session.
## How to Execute a Step
@@ -9,7 +9,8 @@ Start a fresh Claude Code session and prompt, for example: "Execute docs/plan/01
The executing session should:
1. Read this file, `00-legacy-analysis.md`, and the step file.
2. Read the referenced parts of `legacy/gitTally` only if the step file says so.
2. Read the referenced parts of the legacy bash script only if the step file says so.
It was removed from the tree with the rename to Werkator; its last state is `git show 7f55068^:legacy/gitTally`, and `00-legacy-analysis.md` condenses it.
3. Implement with tests, following `CLAUDE.md` conventions.
4. Run `./gradlew ktlintFormat` and then `./gradlew build` until green.
5. Update the step's checkbox below and note deviations inside the step file.
@@ -22,16 +23,16 @@ The executing session should:
- The web UI must never get stuck loading (JSON status endpoints with explicit error states instead of regex-rewritten HTML).
- Do not port orphaned or half-implemented legacy config options (see `00-legacy-analysis.md`).
- Every step leaves the build green and the application runnable.
- Config keys added by a step must be updated in three places: `GitTallyConfig`, the `InitCommand` templates, and `docs/configuration.md`.
- Config keys added by a step must be updated in three places: `WerkatorConfig`, the `InitCommand` templates, and `docs/configuration.md`.
## Proposed Architecture Decisions
These are proposals baked into the steps.
Revisit them in an ADR if a step uncovers problems.
- Build results are persisted as a JSON file under `.git/gittally/`, behind a `BuildResultRepository` interface (no database, but replaceable).
- Build results are persisted as a JSON file under `.git/werkator/`, behind a `BuildResultRepository` interface (no database, but replaceable).
- Builds run concurrently up to `builds.maxConcurrent` (default 1), but never more than one build per branch at a time.
Each branch builds in its own reusable git worktree under `.git/gittally/worktrees/`, checked out detached at the requested commit — never in the primary checkout.
Each branch builds in its own reusable git worktree under `.git/werkator/worktrees/`, checked out detached at the requested commit — never in the primary checkout.
A later step must decide (possibly per config) whether a new commit on a branch cancels that branch's running build or waits for it; for now new builds queue behind the running one.
- Artifacts stay on the filesystem, served by the Spring server.
- The web UI is server-rendered HTML plus small JavaScript polling JSON endpoints (no SPA framework).
@@ -76,7 +77,7 @@ Added after the 2026-08-10 overhead measurements on vm2176:
Added for the vm2176 → vm4006 migration (2026-08-10):
- [x] `15-runtime-bundle-distribution.md` — self-contained runtime bundle (jlink JRE + jar) for hosts without a Java runtime
- [x] `16-git-in-docker-builds.md` — read-only git metadata inside Docker build containers, with `.git/gittally/` masked
- [x] `16-git-in-docker-builds.md` — read-only git metadata inside Docker build containers, with `.git/werkator/` masked
Added after v0.9.19 replaced the per-branch settings with build definitions (2026-08-29):
@@ -86,9 +87,9 @@ Added after a silent 57-minute fetch outage on vm4006 (2026-08-30):
- [x] `19-watcher-health-in-ui.md` — show an unreachable origin in the web UI instead of only in the journal
Added for running GitTally on Hostsharing Managed Webspaces (2026-08-10):
Added for running Werkator on Hostsharing Managed Webspaces (2026-08-10):
- [ ] `17-bwrap-build-runtime.md`GitTally on a Managed Webspace: bubblewrap user-namespace build sandbox with a prepared rootfs (precondition check first — see the step file), plus web access under a domain via the platform's Apache proxy and Let's Encrypt
- [ ] `17-bwrap-build-runtime.md`Werkator on a Managed Webspace: bubblewrap user-namespace build sandbox with a prepared rootfs (precondition check first — see the step file), plus web access under a domain via the platform's Apache proxy and Let's Encrypt
Steps 0103 are independent of each other.
Steps 0406 depend on 0103.