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:
co-authored by
Claude Opus 5
parent
7f550689dd
commit
35f06ec1ec
@@ -10,7 +10,7 @@
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
werkator is a greenfield Kotlin/Spring Boot project.
|
||||
Werkator is a greenfield Kotlin/Spring Boot project.
|
||||
A test framework must be chosen before writing any tests.
|
||||
|
||||
The framework shapes how tests are structured, how readable they are, and how well they integrate with the Spring Boot test slice infrastructure.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
werkator is a greenfield Kotlin/Spring Boot project.
|
||||
Werkator is a greenfield Kotlin/Spring Boot project.
|
||||
A Gradle version must be chosen for the initial setup.
|
||||
|
||||
[Gradle 9](https://docs.gradle.org/9.3.0/release-notes.html) (currently 9.5.1) is now stable and available.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
werkator is a greenfield Kotlin/Spring Boot project.
|
||||
Werkator is a greenfield Kotlin/Spring Boot project.
|
||||
A Spring Boot version must be chosen for the initial setup.
|
||||
|
||||
The choice is constrained by the support lifecycle: as of June 2026,
|
||||
|
||||
@@ -64,7 +64,7 @@ nginx/Let's Encrypt container management was not ported; `init --systemd` genera
|
||||
|
||||
#### Disadvantages
|
||||
|
||||
- HTTPS setup is a manual, host-specific step outside werkator's control.
|
||||
- HTTPS setup is a manual, host-specific step outside Werkator's control.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- rejected: -
|
||||
- superseded: -
|
||||
|
||||
**Decision [accepted]:** werkator optionally manages an nginx+certbot Docker container for hosts without a usable reverse proxy — revises the "no managed nginx" part of ADR 0004; deployment behind an existing reverse proxy stays the default.
|
||||
**Decision [accepted]:** Werkator optionally manages an nginx+certbot Docker container for hosts without a usable reverse proxy — revises the "no managed nginx" part of ADR 0004; deployment behind an existing reverse proxy stays the default.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
@@ -15,16 +15,16 @@ That decision was carried over from the rewrite plan without validating it again
|
||||
|
||||
### Technical Background
|
||||
|
||||
werkator must run on Hostsharing managed container environments.
|
||||
These hosts provide Docker but no root access and no host web server that werkator could sit behind.
|
||||
Without the managed nginx container, werkator cannot be served over HTTPS there at all.
|
||||
Werkator must run on Hostsharing managed container environments.
|
||||
These hosts provide Docker but no root access and no host web server that Werkator could sit behind.
|
||||
Without the managed nginx container, Werkator cannot be served over HTTPS there at all.
|
||||
The legacy script already solved this: it wrote an nginx config, ran an nginx Docker container, and obtained/renewed Let's Encrypt certificates via a certbot container in webroot mode.
|
||||
|
||||
## Considered Options
|
||||
|
||||
* Keep ADR 0004 as is (host reverse proxy only)
|
||||
* Re-add the legacy managed nginx+certbot container as an opt-in feature
|
||||
* External tooling (user-maintained compose stack next to werkator)
|
||||
* External tooling (user-maintained compose stack next to Werkator)
|
||||
|
||||
### Host reverse proxy only
|
||||
|
||||
@@ -38,7 +38,7 @@ The legacy script already solved this: it wrote an nginx config, ran an nginx Do
|
||||
|
||||
### Opt-in managed nginx+certbot container
|
||||
|
||||
werkator starts and supervises a labelled nginx container and handles certificate issuance/renewal via certbot, only when explicitly enabled in the config.
|
||||
Werkator starts and supervises a labelled nginx container and handles certificate issuance/renewal via certbot, only when explicitly enabled in the config.
|
||||
|
||||
#### Advantages
|
||||
|
||||
@@ -54,7 +54,7 @@ werkator starts and supervises a labelled nginx container and handles certificat
|
||||
|
||||
#### Advantages
|
||||
|
||||
- Keeps werkator itself simple.
|
||||
- Keeps Werkator itself simple.
|
||||
|
||||
#### Disadvantages
|
||||
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
- rejected: -
|
||||
- superseded: -
|
||||
|
||||
**Decision [accepted]:** werkator is distributed for hosts without a Java runtime as a self-contained runtime bundle — a jlink-trimmed JRE plus `werkator.jar` plus a launcher script in one tarball, built by `./gradlew runtimeBundle`.
|
||||
The JAR stays the primary artifact; a GraalVM native image and a containerized werkator runtime were rejected.
|
||||
**Decision [accepted]:** Werkator is distributed for hosts without a Java runtime as a self-contained runtime bundle — a jlink-trimmed JRE plus `werkator.jar` plus a launcher script in one tarball, built by `./gradlew runtimeBundle`.
|
||||
The JAR stays the primary artifact; a GraalVM native image and a containerized Werkator runtime were rejected.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
werkator must run on Hostsharing container servers (the primary target, see ADR 0005).
|
||||
Werkator must run on Hostsharing container servers (the primary target, see ADR 0005).
|
||||
These hosts provide git, Docker, make, and systemd user sessions, but no Java runtime, and nothing may be installed system-wide.
|
||||
`docs/bootstrapping.md` sketched a containerized werkator runtime as the future answer; that sketch was never validated against the operational details.
|
||||
`docs/bootstrapping.md` sketched a containerized Werkator runtime as the future answer; that sketch was never validated against the operational details.
|
||||
|
||||
## Considered Options
|
||||
|
||||
* jlink runtime bundle (trimmed JRE + jar + launcher, one tarball)
|
||||
* GraalVM native image (single executable)
|
||||
* Containerized werkator runtime (the original `docs/bootstrapping.md` sketch)
|
||||
* Containerized Werkator runtime (the original `docs/bootstrapping.md` sketch)
|
||||
|
||||
### jlink Runtime Bundle
|
||||
|
||||
@@ -44,12 +44,12 @@ Bad:
|
||||
|
||||
### GraalVM Native Image
|
||||
|
||||
Rejected because Spring AOT evaluates bean conditions at build time, and werkator's dual-mode wiring cannot be represented in a single AOT arrangement:
|
||||
Rejected because Spring AOT evaluates bean conditions at build time, and Werkator's dual-mode wiring cannot be represented in a single AOT arrangement:
|
||||
the CLI context runs without web and with `@Profile("!server")` `CliRunner`, while the `server` subcommand starts a second `SpringApplication` with `WebApplicationType.SERVLET` and the `server` profile gating the watcher/metrics/nginx lifecycles.
|
||||
Whichever profile and web type the AOT processing fixes, the other mode's beans are missing from the binary.
|
||||
Supporting both 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, on top of the usual native-image reflection work (Jackson-bound config and persistence classes, picocli).
|
||||
|
||||
### Containerized werkator Runtime
|
||||
### Containerized Werkator Runtime
|
||||
|
||||
Rejected for operational complexity: the image must bundle git and docker CLIs; the container needs a same-path `$HOME` mount plus a docker-socket mount and uid/gid mapping so that `DockerBuildRunner`'s `--volume $workspace:$workspace` sibling mounts keep working; and the systemd unit must be hand-edited to a `docker run` invocation.
|
||||
This remains the documented fallback if the runtime bundle ever becomes unworkable.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
werkator's configuration is branch-centric: `branches.<name>` holds the build settings, and the nightly schedule (`autoBuild`) hangs off the branch.
|
||||
Werkator's configuration is branch-centric: `branches.<name>` holds the build settings, and the nightly schedule (`autoBuild`) hangs off the branch.
|
||||
v0.9.13 added a per-slot `buildCommand` and `name` to `autoBuild.times[]`, so a nightly slot could run a fuller check recorded in its own result pool.
|
||||
That worked, but it is a job concept hidden inside a schedule entry: the slot carries a command, an identity, and (implicitly) a branch — everything a job has, in the wrong place.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# werkator Bootstrapping
|
||||
# Werkator Bootstrapping
|
||||
|
||||
Bootstrapping prepares a git repository for use with werkator.
|
||||
It creates the config files described in [configuration.md](configuration.md).
|
||||
@@ -30,14 +30,14 @@ java -jar <werkator-root>/build/libs/werkator.jar init
|
||||
|
||||
### 1. Detect the Repository Root
|
||||
|
||||
werkator resolves the repository root by running `git rev-parse --show-toplevel`.
|
||||
Werkator resolves the repository root by running `git rev-parse --show-toplevel`.
|
||||
If the current directory is not inside a git repository, `init` exits with an error.
|
||||
|
||||
### 2. Auto-detect Gitea Connection from `origin`
|
||||
|
||||
If `gitea.baseUrl`, `gitea.owner`, and `gitea.repo` are already set in `.werkator.yml`, these values are used.
|
||||
|
||||
Otherwise, werkator inspects the `origin` remote URL and derives the Gitea connection defaults:
|
||||
Otherwise, Werkator inspects the `origin` remote URL and derives the Gitea connection defaults:
|
||||
|
||||
| Origin URL form | Detected values |
|
||||
|--------------------------------------------|----------------------------------------|
|
||||
@@ -84,7 +84,7 @@ gitea:
|
||||
...
|
||||
```
|
||||
|
||||
Then, you have to configure *werkator* by amending this config file according to [configuration.md](configuration.md).
|
||||
Then, you have to configure *Werkator* by amending this config file according to [configuration.md](configuration.md).
|
||||
|
||||
## Output
|
||||
|
||||
@@ -104,9 +104,9 @@ Or, when files already exist:
|
||||
|
||||
## Hosts Without a Java Runtime
|
||||
|
||||
werkator is intended to run on Hostsharing Container Server environments, which provide Docker and git but no Java runtime.
|
||||
Werkator is intended to run on Hostsharing Container Server environments, which provide Docker and git but no Java runtime.
|
||||
For these hosts, `./gradlew runtimeBundle` builds a self-contained runtime bundle (jlink-trimmed JRE + JAR + launcher) — see [deployment.md](deployment.md) and ADR 0006.
|
||||
A containerized werkator runtime was considered and rejected there.
|
||||
A containerized Werkator runtime was considered and rejected there.
|
||||
|
||||
## Next Steps After `init`
|
||||
|
||||
@@ -124,15 +124,15 @@ A containerized werkator runtime was considered and rejected there.
|
||||
|
||||
## Example: Test Server with a Fake Build
|
||||
|
||||
[examples/setup-werkator-testserver.sh](examples/setup-werkator-testserver.sh) starts a werkator server against a scratch repository with a fake build — the setup used for the manual UI/API smoke tests during development.
|
||||
[examples/setup-werkator-testserver.sh](examples/setup-werkator-testserver.sh) starts a Werkator server against a scratch repository with a fake build — the setup used for the manual UI/API smoke tests during development.
|
||||
It creates a local bare origin plus a `work` clone, commits a slow fake build script (live log output, demo report artifact) with a `pollInterval: 5s` config, and starts the server on port 18980.
|
||||
The origin gets a second branch (`feature/demo`), so the Branches view shows more than one entry.
|
||||
No Gitea, no credentials, no Docker; `INSTALL_DIR`, `SERVER_PORT`, and `BUILD_SECONDS` can be overridden via environment variables.
|
||||
While the server runs, push empty commits from the `work` clone to trigger builds; a commit message containing `[fail]` makes the build fail, and pushing a new branch exercises the new-origin-branch path.
|
||||
|
||||
## Example: Self-Hosting werkator
|
||||
## Example: Self-Hosting Werkator
|
||||
|
||||
[examples/setup-werkator-selfhost.sh](examples/setup-werkator-selfhost.sh) shows the full sequence as a runnable script: it sets up a werkator instance that watches and builds werkator itself.
|
||||
[examples/setup-werkator-selfhost.sh](examples/setup-werkator-selfhost.sh) shows the full sequence as a runnable script: it sets up a Werkator instance that watches and builds Werkator itself.
|
||||
Run it from a working checkout; it builds the JAR, creates a dedicated clone, runs `init`, writes the machine-specific config, and starts the server.
|
||||
`INSTALL_DIR`, `ORIGIN_URL`, `SERVER_PORT`, `GIT_ACCOUNT`, and `GIT_TOKEN` can be overridden via environment variables.
|
||||
The script also demonstrates the kick-start trick: resetting the local ref one commit behind origin makes the very first poll build immediately, instead of waiting for the next push.
|
||||
|
||||
+29
-24
@@ -1,6 +1,6 @@
|
||||
# werkator Configuration Reference
|
||||
# Werkator Configuration Reference
|
||||
|
||||
werkator is configured via YAML files. Settings are merged from several sources in order — later layers override earlier ones.
|
||||
Werkator is configured via YAML files. Settings are merged from several sources in order — later layers override earlier ones.
|
||||
|
||||
## Config File Locations
|
||||
|
||||
@@ -12,36 +12,41 @@ werkator is configured via YAML files. Settings are merged from several sources
|
||||
|
||||
The repo install config (`.git/werkator/.werkator.yml`) wins on any key present in both files. Typically used to set `git.token` and `git.account` without committing them.
|
||||
|
||||
### Which werkator a file is written for
|
||||
Each of the three is also looked up under the name it had before the rename to Werkator, spelled exactly as it was: `.gittally.yml` at the repository root and in a build worktree, `.git/gittally/.gittally.yml` for the machine layer.
|
||||
The current name always wins; where both exist the old file is ignored, never merged.
|
||||
The fallback exists because a missing configuration is not an error — it leaves every setting at its default, so an installation that updated without moving its files would come up looking healthy while having forgotten its credentials and its builds.
|
||||
Rename the files at your convenience; the fallback goes away with a future release.
|
||||
|
||||
Every configuration file may declare the werkator it was written for. Without it, a
|
||||
### Which Werkator a file is written for
|
||||
|
||||
Every configuration file may declare the Werkator it was written for. Without it, a
|
||||
version that renames or drops a key does not fail — it silently ignores what it no longer
|
||||
understands, and the effect shows up as a build that does the wrong thing.
|
||||
|
||||
```yaml
|
||||
werkator:
|
||||
version:
|
||||
since: "0.9.16" # enforced: an older werkator refuses to read this file
|
||||
below: "2.0" # your release marker; werkator decides how strictly to take it
|
||||
since: "0.9.16" # enforced: an older Werkator refuses to read this file
|
||||
below: "2.0" # your release marker; Werkator decides how strictly to take it
|
||||
```
|
||||
|
||||
There is deliberately **no version of the file format** (no `apiVersion`): no API is
|
||||
involved — werkator reads its own configuration — and only one configuration generation is
|
||||
involved — Werkator reads its own configuration — and only one configuration generation is
|
||||
ever supported. The declaration exists to make an incompatibility nameable, never to run
|
||||
two parsers.
|
||||
|
||||
`since` is a hard floor and covers both directions:
|
||||
|
||||
- a newer file on an older werkator is refused instead of being half-understood;
|
||||
- a newer file on an older Werkator is refused instead of being half-understood;
|
||||
- a file written *before* a breaking change and read *after* it is refused as well —
|
||||
werkator knows in which version its configuration format last broke, so the message can
|
||||
name the change: *"is written for werkator 1.4.0, but the configuration format changed
|
||||
Werkator knows in which version its configuration format last broke, so the message can
|
||||
name the change: *"is written for Werkator 1.4.0, but the configuration format changed
|
||||
incompatibly in 2.0.0: `builds:` is now `buildSpec:`"*.
|
||||
|
||||
`below` is optional and names the first version this file was **not** released for. The
|
||||
bound is exclusive, so `below: "2.0"` means everything up to 2.0.0. On its own it only
|
||||
warns — a caution marker nobody maintained must never stop a CI. The refusal above comes
|
||||
from werkator's own knowledge of its breaking changes, not from this value. The intended
|
||||
from Werkator's own knowledge of its breaking changes, not from this value. The intended
|
||||
routine is the one known from IDE plugins: a new version appears, the warning shows up, you
|
||||
try it (on a test host, or in production with a rollback ready), and then raise `below` and
|
||||
commit that.
|
||||
@@ -95,7 +100,7 @@ single branch may decide it:
|
||||
configuration does.
|
||||
|
||||
The distinction is documentary.
|
||||
werkator applies one rule: every pinned key is stripped from the branch layer, and the
|
||||
Werkator applies one rule: every pinned key is stripped from the branch layer, and the
|
||||
value then resolves from whichever remaining layer sets it.
|
||||
The names say where a key is meant to live, not how it is enforced.
|
||||
|
||||
@@ -122,14 +127,14 @@ Add `--show-secrets` to print it in clear text.
|
||||
Values shown are the defaults.
|
||||
|
||||
```yaml
|
||||
# The werkator this file is written for (see the section above).
|
||||
# The Werkator this file is written for (see the section above).
|
||||
werkator:
|
||||
version:
|
||||
since: "0.9.18" # enforced: older werkator refuses this file
|
||||
since: "0.9.18" # enforced: older Werkator refuses this file
|
||||
below: "2.0" # optional release marker; warns, does not block
|
||||
|
||||
server:
|
||||
# Public base URL of this werkator installation — used for all links posted to Gitea.
|
||||
# Public base URL of this Werkator installation — used for all links posted to Gitea.
|
||||
publicBaseUrl: https://ci.example.org/
|
||||
# HTTP port of the `server` subcommand (default 18080, like legacy)
|
||||
port: 18080
|
||||
@@ -275,13 +280,13 @@ watcher:
|
||||
### Notes on `server.bindAddress`
|
||||
|
||||
The default is `127.0.0.1`.
|
||||
Neither the web UI nor the JSON API authenticates read access — which is intended, so build states and artifacts can be linked from anywhere — so werkator is meant to sit behind the host's reverse proxy rather than on a public interface.
|
||||
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches werkator over the Docker bridge, not over loopback), or when the proxy runs on another host.
|
||||
Neither the web UI nor the JSON API authenticates read access — which is intended, so build states and artifacts can be linked from anywhere — so Werkator is meant to sit behind the host's reverse proxy rather than on a public interface.
|
||||
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches Werkator over the Docker bridge, not over loopback), or when the proxy runs on another host.
|
||||
Installations created before v0.9.9 have `bindAddress: 0.0.0.0` written into their `.werkator.yml` and keep it; the new default only applies where the key is absent or `init` writes a fresh file.
|
||||
|
||||
### Notes on `server.nginx`
|
||||
|
||||
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves werkator over HTTPS (ADR 0005).
|
||||
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves Werkator over HTTPS (ADR 0005).
|
||||
This is meant for hosts that provide Docker but no usable reverse proxy (e.g. Hostsharing managed containers); otherwise prefer the reverse-proxy setup in [deployment.md](deployment.md).
|
||||
Certificates are obtained and renewed via Let's Encrypt (certbot Docker container, webroot mode), so `serverName` must be a public DNS name pointing at the host and `httpPort` must be reachable from the internet as port 80 (or via a port forward).
|
||||
When `server.publicBaseUrl` is empty and `serverName` is set, it defaults to `https://<serverName>/`.
|
||||
@@ -334,7 +339,7 @@ Triggers: `onPush: true` builds every new commit of the selected branches; `atTi
|
||||
A slot may also be written as `??:MM` — that minute of every hour, expanded to its 24 slots, so the build runs hourly.
|
||||
Only the latest due slot of a day triggers, so slots missed while the server was down are skipped instead of piling up, and a slot whose pool is still building is retried on the next poll cycle until it succeeds.
|
||||
A definition may have both; one with neither never triggers automatically — which is how `builds.default` is written when it is meant as a settings base only.
|
||||
werkator logs a warning once when no definition has a trigger at all, because such an instance never builds anything on its own.
|
||||
Werkator logs a warning once when no definition has a trigger at all, because such an instance never builds anything on its own.
|
||||
|
||||
Selector: `trigger.branches` lists branch names or glob patterns (`*` matches any characters, also across `/`); empty selects all origin branches.
|
||||
A pattern prefixed with `!` excludes instead, and an exclusion always wins regardless of order — `["*", "!master"]` is every branch but master.
|
||||
@@ -343,7 +348,7 @@ That is how a branch gets a build of its own without being built by the default
|
||||
Both parts combine as an intersection.
|
||||
|
||||
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` with all its keys.
|
||||
A definition carries the complete description of its build; unset keys fall back to `builds.default` and then to werkator's own defaults.
|
||||
A definition carries the complete description of its build; unset keys fall back to `builds.default` and then to Werkator's own defaults.
|
||||
`requirePullRequest`, `statusContext`, `docker.enabled`, and `docker.network` are pinned (master-pinned, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci)): they are read from the repo install/project config even when a branch sets them in its own committed config.
|
||||
Inheritance from `builds.default` covers the settings only — the `trigger` block says when and where *this* build runs and is never inherited.
|
||||
Definitions are part of the branch layer: a branch may add its own and override those from the project config, for its own builds only.
|
||||
@@ -378,7 +383,7 @@ To migrate, move `branches.default` to `builds.default`, add `onPush: true`, and
|
||||
### Notes on `watcher.fastForwardLocalRefs`
|
||||
|
||||
Builds run in worktrees that share the primary checkout's `.git`, so a build tool can read `refs/heads/*` there.
|
||||
werkator itself never needs those refs to be current — it builds the commit `refs/remotes/origin/<branch>` points at — but build tools do.
|
||||
Werkator itself never needs those refs to be current — it builds the commit `refs/remotes/origin/<branch>` points at — but build tools do.
|
||||
A common case is a check that refuses to run when the local main branch differs from its origin counterpart; without this key it would fail on every build once origin moved on, because nothing would ever advance the local ref.
|
||||
|
||||
The fast-forward runs at the end of the poll cycle, after the due branches were enqueued.
|
||||
@@ -390,15 +395,15 @@ The branch checked out in the primary checkout is advanced with `git merge --ff-
|
||||
|
||||
### Notes on `builds.<name>.docker`
|
||||
|
||||
With `docker.enabled`, werkator shells out to the `docker` CLI; the `docker` command must be on the `PATH`.
|
||||
With `docker.enabled`, Werkator shells out to the `docker` CLI; the `docker` command must be on the `PATH`.
|
||||
When `dockerfile` is set, the image is (re)built whenever the Dockerfile content, its path, or the context path changed.
|
||||
Staleness is tracked via the image label `org.werkator.build-inputs-sha256`.
|
||||
A Gradle cache volume `werkator-gradle-<repo-key>` is created per repository and mounted as `GRADLE_USER_HOME`.
|
||||
The build worktree is bind-mounted into the container; after each command the ownership of `build/` and `.gradle/` is repaired to the host user.
|
||||
Git works inside the container: the primary repository's `.git` is mounted read-only (so build steps can run read-only git commands like `git log` or `git describe`), with `.git/werkator/` masked by an empty tmpfs so the build can never read the machine config (`git.token`) or the control token.
|
||||
Note that the rest of `.git` — including `.git/config` — is visible to builds; werkator never stores credentials there, and neither should you.
|
||||
Note that the rest of `.git` — including `.git/config` — is visible to builds; Werkator never stores credentials there, and neither should you.
|
||||
The Docker socket is mounted into the container and `DOCKER_HOST`/`TESTCONTAINERS_*` variables are set, so Testcontainers-based builds work inside the container.
|
||||
All werkator containers carry `org.hoennig.werkator` labels; stale build containers of the repository are removed before the first Docker build after a restart.
|
||||
All Werkator containers carry `org.hoennig.werkator` labels; stale build containers of the repository are removed before the first Docker build after a restart.
|
||||
|
||||
## `.git/werkator/.werkator.yml` (not committed)
|
||||
|
||||
|
||||
+13
-13
@@ -1,8 +1,8 @@
|
||||
# werkator Deployment
|
||||
# Werkator Deployment
|
||||
|
||||
This document describes how to run werkator as a permanent service.
|
||||
This document describes how to run Werkator as a permanent service.
|
||||
The recommended setup is a systemd user service behind an existing reverse proxy.
|
||||
By default werkator does not manage nginx or TLS certificates itself; it relies on the host's existing web server and certbot.
|
||||
By default Werkator does not manage nginx or TLS certificates itself; it relies on the host's existing web server and certbot.
|
||||
For hosts without one, an opt-in managed nginx/TLS container is available, see [Hosts Without a Reverse Proxy](#hosts-without-a-reverse-proxy-managed-nginxtls).
|
||||
|
||||
## Prerequisites
|
||||
@@ -34,7 +34,7 @@ So always run it via the stable path, not via `build/libs/`.
|
||||
|
||||
## Install the Service
|
||||
|
||||
Initialize werkator in the repository to watch (see [bootstrapping.md](bootstrapping.md) for details):
|
||||
Initialize Werkator in the repository to watch (see [bootstrapping.md](bootstrapping.md) for details):
|
||||
|
||||
```bash
|
||||
cd /path/to/repo
|
||||
@@ -68,7 +68,7 @@ The unit name contains the repository name, so several repositories can be serve
|
||||
The `werkator-docker-prune.timer` runs `docker system prune -af` every night at 02:00 (host time), before the usual auto-build slots.
|
||||
It removes stopped containers, unused images, unused networks, and dangling build cache, so nightly builds start from freshly built images.
|
||||
Unlike the legacy cleanup it does **not** prune volumes — the per-repository Gradle cache volumes survive.
|
||||
The units are host-global (no repository name): with several werkator instances on one host, every `init --systemd` generates the same files and the symlinks coincide.
|
||||
The units are host-global (no repository name): with several Werkator instances on one host, every `init --systemd` generates the same files and the symlinks coincide.
|
||||
On hosts without a `docker` CLI the service is skipped, not failed (`ExecCondition`).
|
||||
`Persistent=true` catches up a missed run after downtime.
|
||||
|
||||
@@ -136,7 +136,7 @@ Config file changes are not needed for an update; new keys take their defaults.
|
||||
## Control Token
|
||||
|
||||
Viewing is public by design: build states, logs and artifacts are readable without any login, so they can be linked from Gitea, chats or tickets.
|
||||
That is safe as long as the builds themselves handle no real secrets — werkator has no per-endpoint gating, so an installation whose build output could contain credentials must stay off the public internet (reverse proxy with access control, or `server.bindAddress: 127.0.0.1`).
|
||||
That is safe as long as the builds themselves handle no real secrets — Werkator has no per-endpoint gating, so an installation whose build output could contain credentials must stay off the public internet (reverse proxy with access control, or `server.bindAddress: 127.0.0.1`).
|
||||
Only the three mutating actions — restart, cancel, delete — require the control token from `.git/werkator/control-token`, a random secret the server generates on first start (mode `0600`; delete the file to rotate it).
|
||||
|
||||
The token is never embedded in a page.
|
||||
@@ -158,12 +158,12 @@ curl -X POST -H "X-werkator-Token: $(cat .git/werkator/control-token)" \
|
||||
|
||||
`.git/werkator/werkator.env` is loaded by the unit as `EnvironmentFile`.
|
||||
It only tunes the JVM process, e.g. `JAVA_OPTS=-Xmx256m`.
|
||||
All werkator configuration lives in the YAML files described in [configuration.md](configuration.md), not in environment variables.
|
||||
All Werkator configuration lives in the YAML files described in [configuration.md](configuration.md), not in environment variables.
|
||||
`init --systemd` never overwrites an existing environment file.
|
||||
|
||||
## Reverse Proxy (nginx)
|
||||
|
||||
Bind werkator to localhost — the default since v0.9.9 — and set the public URL in `.werkator.yml`:
|
||||
Bind Werkator to localhost — the default since v0.9.9 — and set the public URL in `.werkator.yml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
@@ -204,7 +204,7 @@ This replaces the legacy script's managed nginx/Let's Encrypt Docker container f
|
||||
## Hosts Without a Java Runtime (Runtime Bundle)
|
||||
|
||||
Some hosts provide git and Docker but no Java runtime and no way to install one, e.g. Hostsharing container servers.
|
||||
For these, werkator ships as a self-contained runtime bundle: a jlink-trimmed JRE, `werkator.jar`, and a launcher script in one tarball (ADR 0006).
|
||||
For these, Werkator ships as a self-contained runtime bundle: a jlink-trimmed JRE, `werkator.jar`, and a launcher script in one tarball (ADR 0006).
|
||||
|
||||
Build the bundle on a Linux x86_64 machine whose glibc is not newer than the target's:
|
||||
|
||||
@@ -231,12 +231,12 @@ cd /path/to/repo
|
||||
`init --systemd` detects the bundle automatically: the generated unit's `ExecStart` points at the bundle's `jre/bin/java` and `lib/werkator.jar`, so the install commands printed by `init --systemd` work unchanged.
|
||||
`JAVA_OPTS` from the environment file applies as usual.
|
||||
|
||||
To update werkator, stop the service, unpack the new bundle over `~/opt/werkator`, and restart the service.
|
||||
To update Werkator, stop the service, unpack the new bundle over `~/opt/werkator`, and restart the service.
|
||||
|
||||
## Hosts Without a Reverse Proxy (Managed nginx/TLS)
|
||||
|
||||
Some hosts provide Docker but no root access and no host web server, e.g. Hostsharing managed container environments.
|
||||
For these, werkator can manage its own nginx+certbot Docker container (ADR 0005).
|
||||
For these, Werkator can manage its own nginx+certbot Docker container (ADR 0005).
|
||||
This is opt-in; where a host web server exists, prefer the reverse-proxy setup above.
|
||||
|
||||
Enable it in the server section of the configuration:
|
||||
@@ -252,12 +252,12 @@ server:
|
||||
letsencryptEmail: admin@example.org
|
||||
```
|
||||
|
||||
On server start, werkator writes the nginx configuration, starts a labelled nginx container publishing `httpPort` and `httpsPort`, obtains a Let's Encrypt certificate via a certbot container (webroot mode), and restarts nginx with the full HTTPS configuration.
|
||||
On server start, Werkator writes the nginx configuration, starts a labelled nginx container publishing `httpPort` and `httpsPort`, obtains a Let's Encrypt certificate via a certbot container (webroot mode), and restarts nginx with the full HTTPS configuration.
|
||||
A renewal check runs daily; certificates and nginx state persist in `server.nginx.stateDir` across restarts.
|
||||
On shutdown the container is removed.
|
||||
All nginx and certificate failures are non-fatal warnings — the plain HTTP server keeps running without the proxy.
|
||||
|
||||
`serverName` must be a public DNS name pointing at the host, reachable from the internet on port 80/443 (directly or via a port forward to `httpPort`/`httpsPort`), otherwise the ACME challenge fails.
|
||||
The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers.
|
||||
With the managed nginx, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes werkator unreachable for the proxy container.
|
||||
With the managed nginx, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes Werkator unreachable for the proxy container.
|
||||
See [configuration.md](configuration.md) for all `server.nginx.*` keys.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Example: set up a werkator instance that watches and builds werkator itself.
|
||||
# Run from inside a working checkout of the werkator repository.
|
||||
# Example: set up a Werkator instance that watches and builds Werkator itself.
|
||||
# Run from inside a working checkout of the Werkator repository.
|
||||
#
|
||||
# Usage:
|
||||
# GIT_ACCOUNT=mi GIT_TOKEN=xxxx ./docs/examples/setup-werkator-selfhost.sh
|
||||
@@ -15,7 +15,7 @@ SERVER_PORT="${SERVER_PORT:-18080}"
|
||||
DEV_CHECKOUT=$(git rev-parse --show-toplevel)
|
||||
ORIGIN_URL="${ORIGIN_URL:-$(git -C "$DEV_CHECKOUT" remote get-url origin)}"
|
||||
|
||||
# 1. Build the werkator jar — the last Gradle run you ever start by hand.
|
||||
# 1. Build the Werkator jar — the last Gradle run you ever start by hand.
|
||||
(cd "$DEV_CHECKOUT" && ./gradlew --console=plain build)
|
||||
|
||||
# 2. Dedicated clone: builds run in worktrees under its .git/werkator/worktrees,
|
||||
@@ -43,17 +43,17 @@ server:
|
||||
EOF
|
||||
chmod 600 .git/werkator/.werkator.yml
|
||||
|
||||
# The committed .werkator.yml already builds werkator itself:
|
||||
# The committed .werkator.yml already builds Werkator itself:
|
||||
# buildCommand: ./gradlew --console=plain --no-daemon test
|
||||
# artifactDirs: [build/reports]
|
||||
|
||||
# 5. Optional kick-start: put the local ref one commit behind origin so the very
|
||||
# first poll triggers a build — otherwise werkator waits for the next push.
|
||||
# first poll triggers a build — otherwise Werkator waits for the next push.
|
||||
# Builds never move this ref or touch this checkout, so lagging is harmless.
|
||||
git reset --hard --quiet HEAD~1 || true
|
||||
|
||||
# 6. Run it (Ctrl-C stops it cleanly). For a permanent setup, run
|
||||
# `java -jar "$INSTALL_DIR/werkator.jar" init --systemd` here instead and follow
|
||||
# docs/deployment.md — the generated unit points at this jar and repo.
|
||||
echo "werkator self-host: http://localhost:$SERVER_PORT/ — watching $ORIGIN_URL"
|
||||
echo "Werkator self-host: http://localhost:$SERVER_PORT/ — watching $ORIGIN_URL"
|
||||
exec java -jar "$INSTALL_DIR/werkator.jar" server
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Example: start a werkator test server watching a scratch repository with a fake build.
|
||||
# Example: start a Werkator test server watching a scratch repository with a fake build.
|
||||
# This is the setup used for the manual UI/API smoke tests during development:
|
||||
# a local bare origin (with a second branch for the Branches view), a slow fake
|
||||
# build with live log output and a demo report artifact, and a fast poll
|
||||
@@ -21,7 +21,7 @@ export BUILD_SECONDS="${BUILD_SECONDS:-25}" # inherited by the build process a
|
||||
|
||||
DEV_CHECKOUT=$(git rev-parse --show-toplevel)
|
||||
|
||||
# 1. Build the werkator jar.
|
||||
# 1. Build the Werkator jar.
|
||||
(cd "$DEV_CHECKOUT" && ./gradlew --console=plain build)
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cp "$DEV_CHECKOUT/build/libs/werkator.jar" "$INSTALL_DIR/werkator.jar"
|
||||
@@ -39,7 +39,7 @@ if ! git -C "$INSTALL_DIR/work" rev-parse --quiet --verify HEAD >/dev/null; then
|
||||
cat > "$INSTALL_DIR/work/fake-build.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Fake build: visible progress for the live log, then a demo report artifact.
|
||||
# werkator exports `branch`; BUILD_SECONDS is inherited from the server process.
|
||||
# Werkator exports `branch`; BUILD_SECONDS is inherited from the server process.
|
||||
set -euo pipefail
|
||||
echo "fake build of branch ${branch:-unknown} at commit $(git rev-parse --short HEAD)"
|
||||
if git log -1 --pretty=%s | grep -qF '[fail]'; then
|
||||
@@ -101,7 +101,7 @@ git reset --hard --quiet HEAD~1 || true
|
||||
|
||||
# 5. Run it (Ctrl-C stops it cleanly).
|
||||
echo
|
||||
echo "werkator test server: http://localhost:$SERVER_PORT/"
|
||||
echo "Werkator test server: http://localhost:$SERVER_PORT/"
|
||||
echo "Trigger a build: git -C $INSTALL_DIR/work commit --allow-empty -m 'trigger build' && git -C $INSTALL_DIR/work push"
|
||||
echo "Trigger a failure: same with commit message 'trigger [fail]'"
|
||||
echo
|
||||
@@ -1,94 +0,0 @@
|
||||
# Migration from the Legacy Script
|
||||
|
||||
The bash script `legacy/werkator` is deprecated and replaced by this application.
|
||||
This document maps the legacy environment-variable configuration to the YAML configuration and lists the manual migration steps.
|
||||
See [configuration.md](configuration.md) for the full configuration reference and [deployment.md](deployment.md) for the new service setup.
|
||||
|
||||
## Configuration Mapping
|
||||
|
||||
Legacy configuration came from environment variables (`werkator --env` template, sourced env files).
|
||||
The new configuration lives in two YAML files: `.werkator.yml` (committed) and `.git/werkator/.werkator.yml` (machine-specific, secrets).
|
||||
|
||||
Build-level keys below live in a build definition under `builds.<name>`; use `builds.default` for what used to be
|
||||
the global value — it is the base every other definition inherits its settings from.
|
||||
|
||||
| Legacy environment variable | New YAML key |
|
||||
|---|---|
|
||||
| `werkator_BUILD_COMMAND` | `builds.<name>.buildCommand` |
|
||||
| `werkator_BUILD_CLEAN_COMMAND` | `builds.<name>.cleanCommand` |
|
||||
| `werkator_BUILD_ARTEFACT_DIRS` | `builds.<name>.artifactDirs` — YAML list instead of `;`-separated |
|
||||
| `werkator_BUILD_STDOUT_LOG` | `builds.<name>.stdoutLog` |
|
||||
| `werkator_BUILD_STDERR_LOG` | `builds.<name>.stderrLog` |
|
||||
| `werkator_NEW_BRANCH_COMMIT_MAX_AGE` | `watcher.newBranchMaxAge` |
|
||||
| `werkator_BUILD_DOCKER_IMAGE` | `builds.<name>.docker.image` — also set `docker.enabled: true` (replaces the `--docker` flag) |
|
||||
| `werkator_BUILD_DOCKERFILE` | `builds.<name>.docker.dockerfile` |
|
||||
| `werkator_BUILD_DOCKER_CONTEXT` | `builds.<name>.docker.context` |
|
||||
| `werkator_BUILD_DOCKER_NETWORK` | `builds.<name>.docker.network` — default is now Docker's default network, not `host` |
|
||||
| `werkator_BUILD_DOCKER_ENV` | `builds.<name>.docker.env` — YAML map instead of space-separated assignments |
|
||||
| `werkator_ARTIFACT_SERVER_PORT` | `server.port` |
|
||||
| `werkator_ARTIFACT_SERVER_BIND_ADDRESS` | `server.bindAddress` |
|
||||
| `werkator_ARTIFACT_PUBLIC_BASE_URL` | `server.publicBaseUrl` |
|
||||
| `werkator_ARTIFACT_BUILD_RETENTION_PER_BRANCH` | `artifacts.retentionPerBranch` for a count, `artifacts.retentionMaxAge` for a legacy age value (`h`/`d` suffix); unlike legacy, both limits can be combined |
|
||||
| `werkator_IMPRESSUM_URL` | `server.impressumUrl` |
|
||||
| `werkator_AUTO_BUILD_BRANCHES` | a build definition with `branches: [...]` selecting them |
|
||||
| `werkator_AUTO_BUILD_TIMES` | `builds.<name>.atTimes` — YAML list of UTC `HH:MM` slots |
|
||||
| `werkator_GITEA_BASE_URL` | `gitea.baseUrl` |
|
||||
| `werkator_GITEA_OWNER` | `gitea.owner` |
|
||||
| `werkator_GITEA_REPO` | `gitea.repo` |
|
||||
| `werkator_GITEA_STATUS_CONTEXT` | `gitea.statusContext` |
|
||||
| `werkator_GITEA_GIT_USERNAME` | `git.account` — in `.git/werkator/.werkator.yml` |
|
||||
| `werkator_GITEA_TOKEN` | `git.token` — in `.git/werkator/.werkator.yml`, never committed |
|
||||
| `werkator_ARTIFACT_NGINX_SERVER_NAME` | `server.nginx.serverName` — also set `server.nginx.enabled: true` (replaces the `--nginx` flag) |
|
||||
| `werkator_ARTIFACT_NGINX_HTTP_PORT` | `server.nginx.httpPort` |
|
||||
| `werkator_ARTIFACT_NGINX_HTTPS_PORT` | `server.nginx.httpsPort` |
|
||||
| `werkator_ARTIFACT_NGINX_UPSTREAM_HOST` | `server.nginx.upstreamHost` |
|
||||
| `werkator_ARTIFACT_NGINX_CONTAINER_NAME` | `server.nginx.containerName` |
|
||||
| `werkator_ARTIFACT_NGINX_STATE_DIR` | `server.nginx.stateDir` |
|
||||
| `werkator_ARTIFACT_LETSENCRYPT_EMAIL` | `server.nginx.letsencryptEmail` |
|
||||
|
||||
New keys without a legacy counterpart: `builds.maxConcurrent`, `artifacts.rootDir`, and `watcher.pollInterval`.
|
||||
|
||||
## Intentionally Not Ported
|
||||
|
||||
- Self-install and self-update (`--install`, `--pull`, `werkator_INSTALL_DIR`) — replaced by jar deployment plus `init --systemd`.
|
||||
- `werkator_BUILD_DOCKER_PREFLIGHT_COMMAND` and `werkator_BUILD_DOCKER_JAVA_TOOL_OPTIONS` — hsadmin-ng-specific; use `builds.<name>.docker.env` if needed.
|
||||
- `HSADMIN_NG_*` environment-variable fallbacks.
|
||||
- Env-file configuration itself — the systemd `EnvironmentFile` now only tunes the JVM (`JAVA_OPTS`).
|
||||
- `werkator_GITEA_DELETED_STATUS_DESCRIPTION`, `werkator_BIN_FORWARD`, `werkator_CONFIG_*` — internal legacy mechanics without a counterpart.
|
||||
|
||||
## Build History
|
||||
|
||||
Legacy build history (`.git/git-watch-origin-and-test/build-results.tsv`) is **not** imported; history starts fresh.
|
||||
The formats differ substantially (TSV vs. JSON with commit metadata and artifact keys), and retention would prune imported rows quickly anyway.
|
||||
Old artifacts under the legacy artifact root remain readable on disk until you delete them.
|
||||
|
||||
## Manual Migration Steps
|
||||
|
||||
When migrating to a **different host**, the legacy instance can keep running in parallel until the new one is verified — then skip step 1 here and stop the legacy service on the old host last.
|
||||
During parallel operation, give the new instance a distinct `gitea.statusContext`, so the two instances do not overwrite each other's commit statuses in Gitea.
|
||||
|
||||
Rename the context back to the canonical one **while no build is running**.
|
||||
The Gitea client reads the config per call, so a rename between a build's `running` and its final status splits that build over two contexts: the old one keeps the `pending` "build running" entry forever, and Gitea's combined status of that commit stays *pending* although the build succeeded.
|
||||
Gitea has no API to delete a commit status; the only way out is to post a closing status for the abandoned context by hand:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"state":"success","context":"<old context>","description":"superseded by <new context>"}' \
|
||||
"$GITEA/api/v1/repos/<owner>/<repo>/statuses/<commit-sha>"
|
||||
```
|
||||
|
||||
1. Stop and remove the legacy service:
|
||||
|
||||
```bash
|
||||
systemctl --user disable --now werkator.service
|
||||
rm -f ~/.config/systemd/user/werkator.service
|
||||
systemctl --user daemon-reload
|
||||
```
|
||||
|
||||
2. Build and place the jar as described in [deployment.md](deployment.md).
|
||||
3. In the repository, run `java -jar ~/bin/werkator.jar init`.
|
||||
4. Transfer your settings from the legacy env file into `.werkator.yml` using the table above.
|
||||
5. Put `git.account` and `git.token` into `.git/werkator/.werkator.yml`.
|
||||
6. Verify the effective configuration: `java -jar ~/bin/werkator.jar config:print --full`.
|
||||
7. Install and start the new service: `init --systemd` plus the printed commands, see [deployment.md](deployment.md).
|
||||
8. Optionally clean up legacy state: `.git/git-watch-origin-and-test/` and the legacy artifact root.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 01–03 are independent of each other.
|
||||
Steps 04–06 depend on 01–03.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Migration Plan: GitTally → Werkator
|
||||
|
||||
The rename is a precaution: `gitTally` is the name of another product in the git space.
|
||||
Nothing about what the build system does changes, but the name is part of a running installation in more places than the configuration.
|
||||
This document lists every one of them, says which are handled automatically, and gives the order in which the rest is done.
|
||||
|
||||
Read [deployment.md](deployment.md) for the deployment itself; this plan only covers what the rename adds to it.
|
||||
|
||||
## What Is Handled Automatically
|
||||
|
||||
Every configuration file is looked up under its current name first and under the pre-rename name second, spelled exactly as it was (`config/ConfigFiles.kt`):
|
||||
|
||||
| Layer | current | still accepted |
|
||||
|---|---|---|
|
||||
| Machine config | `.git/werkator/.werkator.yml` | `.git/gittally/.gittally.yml` |
|
||||
| Project config | `.werkator.yml` | `.gittally.yml` |
|
||||
| Branch config, in a build worktree | `.werkator.yml` | `.gittally.yml` |
|
||||
| Branch config, read out of git | `<commit>:.werkator.yml` | `<commit>:.gittally.yml` |
|
||||
|
||||
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 — merging them would revive a setting somebody deliberately dropped while rewriting.
|
||||
|
||||
The fallback exists because a configuration that is not found is not an error.
|
||||
It leaves every setting at its default, so an installation that updated without renaming would come up looking healthy while having forgotten its credentials, its addresses, and what it builds.
|
||||
|
||||
The fallback is temporary and goes away once the watched repositories have been renamed.
|
||||
|
||||
## What Has To Be Moved By Hand
|
||||
|
||||
### 1. The state directory
|
||||
|
||||
`.git/gittally/` → `.git/werkator/`, with everything in it:
|
||||
|
||||
- the machine configuration (found under either name, but it belongs next to the rest)
|
||||
- `build-results.json` — the entire build history
|
||||
- `auto-builds.json` — which scheduled slots already fired today
|
||||
- `control-token` — the token that authorizes mutating API calls
|
||||
- `worktrees/<branchKey>/` — the per-branch build worktrees
|
||||
- the generated systemd unit and its `EnvironmentFile`
|
||||
|
||||
There is no fallback for this path.
|
||||
Without the move the instance starts with an empty history, a fresh control token, and no memory of today's scheduled builds — again without a single failure.
|
||||
|
||||
The worktrees hold absolute paths in both directions (`.git/worktrees/<name>/gitdir` and the worktree's own `.git` file).
|
||||
Either run `git worktree repair` after the move, or simply delete `.git/werkator/worktrees/` — a worktree is rebuilt on the next build of that branch.
|
||||
|
||||
### 2. The artifact root
|
||||
|
||||
Unless `artifacts.rootDir` is set explicitly, artifacts live under `$XDG_STATE_HOME/werkator/artifacts/<repoKey>`, in practice `~/.local/state/werkator/artifacts/<repoKey>`.
|
||||
Move `~/.local/state/gittally/` to `~/.local/state/werkator/`.
|
||||
Left behind, the stored logs and reports of every past build are unreachable, and the permanent latest-green links point at nothing.
|
||||
|
||||
### 3. The systemd units
|
||||
|
||||
The unit names carry the product name:
|
||||
|
||||
| current | before |
|
||||
|---|---|
|
||||
| `werkator-<repo>.service` | `gittally-<repo>.service` |
|
||||
| `werkator.env` | `gittally.env` |
|
||||
| `werkator-docker-prune.service` / `.timer` | `gittally-docker-prune.service` / `.timer` |
|
||||
|
||||
The symlinks in `~/.config/systemd/user/` point into the state directory, so moving that directory breaks them.
|
||||
Disable and remove the old units, regenerate with `werkator init --systemd`, then enable the new ones.
|
||||
The prune units are host-global and shared by all instances on the host — replace them once, not per repository.
|
||||
|
||||
### 4. Docker names and labels
|
||||
|
||||
| what | current | before |
|
||||
|---|---|---|
|
||||
| build container | `werkator-build-<repoKey>-<branchKey>` | `gittally-build-…` |
|
||||
| Gradle cache volume | `werkator-gradle-<repoKey>` | `gittally-gradle-…` |
|
||||
| container label | `org.hoennig.werkator` | `org.hoennig.gittally` |
|
||||
| image input label | `org.werkator.build-inputs-sha256` | `org.gittally.…` |
|
||||
|
||||
The consequences are all one-off and none of them is fatal:
|
||||
|
||||
- A new Gradle cache volume is empty, so the first build after the rename is slow. Rename the volume beforehand if that matters, or accept one cold build.
|
||||
- The changed image label makes the build image rebuild once.
|
||||
- Stale containers from before the rename carry the old label, so the cleanup on restart does not see them. Remove them once by hand.
|
||||
|
||||
### 5. The Gitea check
|
||||
|
||||
`gitea.statusContext` is the name the check appears under in Gitea; the default is now `werkator`.
|
||||
Gitea itself needs no preparation — the context is created implicitly by the first status posted.
|
||||
But a branch protection rule that requires the old context will never be satisfied again, and pull requests wait forever for a check nobody posts.
|
||||
Update the rule in the same step, or leave `statusContext` at the old value until it is.
|
||||
|
||||
Statuses already written keep their old context, so a commit built before and after the change shows both.
|
||||
Werkator also reads the newest status matching the configured context, so the first build after the switch does not see its own earlier results — harmless, at most one extra build.
|
||||
|
||||
### 6. The runtime bundle
|
||||
|
||||
On hosts without a Java runtime the bundle unpacks to `~/opt/werkator/` and its launcher is named `werkator`.
|
||||
Move `~/opt/gittally/` accordingly, or unpack the new bundle fresh and remove the old directory once the service runs.
|
||||
|
||||
### 7. The managed nginx container
|
||||
|
||||
Only where `server.nginx.enabled` is set.
|
||||
The container defaults to `werkator-nginx-<repo>` and its state (certificates included) to `~/.local/state/werkator/nginx/`.
|
||||
Move the state directory with the artifact root, and remove the old container so the new one can take the ports.
|
||||
|
||||
## Order of Work
|
||||
|
||||
Per installation, and only while `/api/builds/current` is `[]` — a running build is interrupted by the restart and re-enqueued, but there is no reason to force that.
|
||||
|
||||
1. `systemctl --user stop werkator-<repo>.service` (old name).
|
||||
2. Move the state directory, the artifact root, and the bundle.
|
||||
3. Repair or delete the worktrees.
|
||||
4. Rename the configuration files at the same time, or leave them to the fallback.
|
||||
5. Deploy the new version.
|
||||
6. `werkator init --systemd`, disable the old units, enable the new ones.
|
||||
7. Start the service.
|
||||
8. Update the Gitea branch protection rule if it names the check.
|
||||
|
||||
## Verification
|
||||
|
||||
- `werkator config:print --full` before the restart: every definition resolves completely, the credentials are there, the docker settings are the host's.
|
||||
- After the start: the build history is the one from before, the watcher polls without errors, no warning about a configuration that was not found.
|
||||
- A branch build starts, runs in the expected image, and reports under the expected Gitea check.
|
||||
|
||||
## Rollback
|
||||
|
||||
Keep the previous bundle and a timestamped copy of the machine configuration.
|
||||
Note the asymmetry: the new version reads both names, the old one only reads the old name.
|
||||
So a rollback works as long as the configuration files still carry — or carry again — their pre-rename names.
|
||||
The state directory has to move back with it.
|
||||
|
||||
## Open Points
|
||||
|
||||
- **Should the state directory get the same fallback as the configuration?**
|
||||
It would make an update a single step, at the price of a second lookup path in every place that writes state.
|
||||
Currently intended as a deliberate manual move, because unlike a configuration the state is written, not only read, and a fallback that writes would have to decide which of two directories wins.
|
||||
- **This repository's own file names** are a separate step: 121 paths still contain `gittally`, package directories included.
|
||||
The rename script for it is written and waits.
|
||||
- **When the fallback goes away**, `ConfigFiles` loses its legacy entries and a leftover `.gittally.yml` should be rejected by name rather than ignored — the same reasoning as for the legacy `branches` section in [plan step 18](plan/18-remove-branches-section.md).
|
||||
|
||||
## Per Host
|
||||
|
||||
### vm4006, `hs.hsadmin.ng`
|
||||
|
||||
- The machine configuration is `~/hs.hsadmin.ng/.git/gittally/.gittally.yml`, mode 600, and it holds the Gitea token — check the mode after every edit, a shell redirect creates 644.
|
||||
- The committed configuration on master still sets `statusContext: GitTally`; it changes with the merge that also renames the file, and that merge needs a colleague's approval.
|
||||
- Deploy only while `/api/builds/current` is `[]`.
|
||||
- Keep the timestamped backups of the machine configuration that already exist next to it.
|
||||
Reference in New Issue
Block a user