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>
This commit is contained in:
mhoennig
2026-08-30 19:39:55 +02:00
co-authored by Claude Opus 5
parent 7f550689dd
commit 35f06ec1ec
156 changed files with 604 additions and 401 deletions
+5 -5
View File
@@ -1,4 +1,4 @@
# Legacy werkator Analysis
# Legacy Werkator Analysis
Condensed analysis of `legacy/werkator` (bash, ~6000 lines) as input for the rewrite.
Line numbers refer to the legacy script at the time of analysis (version 0.7.8).
@@ -90,7 +90,7 @@ No status changes observable during a build (control loop):
Verify need before porting any of these:
- `werkator_BUILD_DOCKER_PREFLIGHT_COMMAND`, `werkator_BUILD_DOCKER_JAVA_TOOL_OPTIONS` — highly hsadmin-ng-specific defaults.
- `werkator_ARTIFACT_NGINX_*`, `werkator_ARTIFACT_LETSENCRYPT_EMAIL` — dropped with nginx management; revived as `server.nginx.*` by step 13 (ADR 0005).
- `werkator_IMPRESSUM_URL` — keep as optional simple footer link if wanted.
- `werkator_INSTALL_DIR` — dropped with self-install.
- `WERKATOR_BUILD_DOCKER_PREFLIGHT_COMMAND`, `WERKATOR_BUILD_DOCKER_JAVA_TOOL_OPTIONS` — highly hsadmin-ng-specific defaults.
- `WERKATOR_ARTIFACT_NGINX_*`, `WERKATOR_ARTIFACT_LETSENCRYPT_EMAIL` — dropped with nginx management; revived as `server.nginx.*` by step 13 (ADR 0005).
- `WERKATOR_IMPRESSUM_URL` — keep as optional simple footer link if wanted.
- `WERKATOR_INSTALL_DIR` — dropped with self-install.
+1 -1
View File
@@ -24,7 +24,7 @@ Configuration comes from `WerkatorConfig` (`gitea.*`, `git.token`).
## Out of Scope
- No callers yet; the build executor (step 04) wires status publishing.
- No webhook receiving; werkator remains poll-based.
- No webhook receiving; Werkator remains poll-based.
## Tests
+2 -2
View File
@@ -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 werkator 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 werkator 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.
+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 `werkatorCommand` 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 werkator 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):
+1 -1
View File
@@ -72,4 +72,4 @@ Manual smoke test (2026-07-07, scratch repo, Rancher Desktop 27.3.1):
- 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 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.
- 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.
+2 -1
View File
@@ -5,7 +5,7 @@ Read `README.md` and `00-legacy-analysis.md` first.
## Goal
Make the new werkator deployable as a service and retire the legacy script.
Make the new Werkator deployable as a service and retire the legacy script.
## Design
@@ -40,6 +40,7 @@ Housekeeping:
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`, `../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:
+3 -3
View File
@@ -6,8 +6,8 @@ Consult `legacy/werkator` for the functions referenced below.
## Goal
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.
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
@@ -38,7 +38,7 @@ Port the legacy nginx subsystem (functions `configure_artifact_nginx_defaults` ~
- 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 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 `werkator_ARTIFACT_NGINX_*`/`werkator_ARTIFACT_LETSENCRYPT_EMAIL` variables.
- `docs/deployment.md` gains a section for hosts without a reverse proxy; `docs/migration-from-legacy.md` maps the `WERKATOR_ARTIFACT_NGINX_*`/`WERKATOR_ARTIFACT_LETSENCRYPT_EMAIL` variables.
## Result (2026-07-08)
+10 -10
View File
@@ -6,8 +6,8 @@ This step revises the "Future: Docker-based Deployment" section of `docs/bootstr
## Goal
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.
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.
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.
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 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.
- **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)
@@ -43,7 +43,7 @@ Deployment (no code changes expected):
- 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 werkator = unpack a new bundle over `~/opt/werkator/` (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:
@@ -99,15 +99,15 @@ 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 (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.
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/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 `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.
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/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/`.
@@ -157,7 +157,7 @@ Shipped fix: a page returning from the background fetches the current state imme
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.
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/werkator.0.9.18.bak` as the rollback copy, no build was running.
@@ -177,5 +177,5 @@ The branch-scoped refusal showed itself in production immediately: `mihoe/reacti
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, werkator failed every fetch for 57 minutes, and the branches view kept showing its last known list as if nothing were wrong.
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).
+2 -2
View File
@@ -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/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.
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; werkator 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
+13 -13
View File
@@ -1,14 +1,14 @@
# Step 17: Running werkator 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 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.
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 werkator 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 werkator 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
@@ -59,7 +59,7 @@ No root on the host, no Docker daemon, no changes to the native and Docker runti
`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. `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.
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
@@ -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 werkator'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,7 +103,7 @@ 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`werkator'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`).
`werkator init --systemd` already generates the unit and the `werkator.env`, whose `JAVA_OPTS=-Xmx…` is what keeps the JVM inside the slice.
@@ -113,9 +113,9 @@ Three platform-side prerequisites, none of them code:
### User model: a dedicated unix user, not the package admin
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.
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 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.
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 werkator 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.** 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.
**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: 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 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.
+1 -2
View File
@@ -30,7 +30,7 @@ Note that `branches:` also exists as the *selector* key **inside** a build defin
## Code
- `config/werkatorConfig.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.
@@ -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
+1 -1
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 werkator 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.
+3 -3
View File
@@ -1,4 +1,4 @@
# werkator Rewrite Plan
# Werkator Rewrite Plan
This directory contains the step-by-step plan for rewriting `legacy/werkator` (bash) as the Kotlin/Spring application in this repository.
Each step file is self-contained and sized for one focused Claude Code session.
@@ -86,9 +86,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 werkator on Hostsharing Managed Webspaces (2026-08-10):
Added for running Werkator on Hostsharing Managed Webspaces (2026-08-10):
- [ ] `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
- [ ] `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.