feat(config): the sandbox section is werkdock, not bwrap

`bwrap` named the mechanism one layer below the tool that actually runs it: since
v1.0.0 Werkator does not invoke bwrap at all, it shells out to the werkdock CLI —
which made `bwrap.werkdock` a key naming its own executor.

The section is `werkdock` now and that key is `werkdock.binary`; BwrapConfig,
BwrapOverrides and BwrapBuildRunner follow the name. A file still writing `bwrap`
is read as before and warned about once per file, in `renameLegacySandbox` on the
raw map of every layer before merging — so nothing downstream knows two names, and
the old name is not a way around the pinning either. Renaming rather than refusing,
because the section lives in the machine configuration of every webspace instance,
which no repository tracks; the hard refusal belongs to the release that sets
ConfigVersions.FORMAT_BROKE_IN, where a file declaring no version can be caught
by name at all.

WERKATOR_SANDBOX in tools/remote follows, and still accepts `bwrap`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-03 20:27:16 +02:00
co-authored by Claude Opus 5
parent 95564587d3
commit 34af8ba1bf
16 changed files with 242 additions and 120 deletions
+11 -10
View File
@@ -71,7 +71,7 @@ above, giving the precedence **branch > repo install > project**. It takes prece
everything that describes how this branch is built: the whole `builds` section — its own
definitions and its overrides of the definitions from the project config, with
`buildCommand`, `cleanCommand`, `artifactDirs`, log file names, and
`docker.image`/`dockerfile`/`context`/`env` and `bwrap.env` inside them. That is how a new configuration is tried out: change it on a branch, and
`docker.image`/`dockerfile`/`context`/`env` and `werkdock.env` inside them. That is how a new configuration is tried out: change it on a branch, and
no other branch's builds are affected.
The branch layer is used in both places where it matters: the watcher reads the committed
@@ -99,7 +99,7 @@ single branch may decide it:
- the repository-side settings: the whole `gitea`, `executor`, and `watcher` sections;
- the trust gate: `requirePullRequest`, and the Gitea status context: `statusContext`;
- the container sandbox policy: `docker.enabled`/`docker.network` and
`bwrap.enabled`/`bwrap.rootfs`/`bwrap.werkdock` — host-pinned as
`werkdock.enabled`/`werkdock.rootfs`/`werkdock.binary` — host-pinned as
long as only the host's configuration sets them, master-pinned once the committed
configuration does.
@@ -406,9 +406,9 @@ That is how a branch gets a build of its own without being built by the default
`activeWithin` (e.g. `24h`) additionally keeps only branches whose origin head commit is younger than the duration — useful to run a nightly deep check over all recently active branches.
Both parts combine as an intersection.
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `bwrap` with all their keys.
Settings: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `requirePullRequest`, `statusContext`, and `docker` and `werkdock` with all their keys.
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`, `docker.network`, `bwrap.enabled`, `bwrap.rootfs`, and `bwrap.werkdock` 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.
`requirePullRequest`, `statusContext`, `docker.enabled`, `docker.network`, `werkdock.enabled`, `werkdock.rootfs`, and `werkdock.binary` 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.
Because the inheritance is applied after all layers are merged, a build a branch invents still inherits the host's `builds.default` — its sandbox policy included, which is what keeps the pinning effective for a build the host has never heard of.
@@ -464,22 +464,23 @@ Note that the rest of `.git` — including `.git/config` — is visible to build
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.
### Notes on `builds.<name>.bwrap`
### Notes on `builds.<name>.werkdock`
With `bwrap.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
With `werkdock.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
This is the third runtime, for hosts without root and without a Docker daemon (e.g. Hostsharing managed webspaces); see `docs/plan/17-bwrap-build-runtime.md` and ADR 0008.
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`bwrap.werkdock`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`werkdock.binary`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
The section was called `bwrap` and its binary key `bwrap.werkdock` until v1.2.0; both are still read, with a warning naming the file, so an installation can be migrated at its next configuration edit rather than at the next update.
`werkdock doctor` checks the host's capability (it replaced the retired `tools/werkator-build-prerequisites.sh` in step 23).
`bwrap.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
`werkdock.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
It is a local path or an `http(s)` URL; a URL is downloaded once into `.git/werkator/buildenv/`.
Build the archive with `tools/build-bwrap-rootfs.sh` on any machine with Docker.
The archive is loaded once per source as the werkdock image `werkator-buildenv-<hash>` into werkdock's store (`$WERKDOCK_HOME`, default `~/.werkdock`) — shared by every repository of this OS user; the hash derives from the source string, so a changed `rootfs` loads a fresh image and stale ones can be removed from the store.
Per-repo Gradle caches persist in `.git/werkator/buildenv/home`, bound as `/root`.
`bwrap.env` adds environment variables inside the sandbox; the environment is otherwise cleared (docker semantics) — the server's environment does not leak in.
`werkdock.env` adds environment variables inside the sandbox; the environment is otherwise cleared (docker semantics) — the server's environment does not leak in.
Files created inside the sandbox are owned by the host user, because uid 0 maps back to the unprivileged webspace user.
`docker` and `bwrap` are mutually exclusive per branch: enabling both is rejected at start, not silently picked.
`docker` and `werkdock` are mutually exclusive per branch: enabling both is rejected at start, not silently picked.
Git works inside the sandbox exactly as inside the Docker container: the primary `.git` is mounted read-only with `.git/werkator/` masked, so builds can run read-only git commands but never reach the machine config or the control token.
## `.git/werkator/.werkator.yml` (not committed)
+2 -2
View File
@@ -185,7 +185,7 @@ The tarball unpacks to a `werkator/` directory, so it must not be extracted over
Rollback is the reverse: stop, remove the new directory (or jar), move `.bak` back, start.
`tools/remote --env-file .env.<instance> werkator instance-update` does the same sequence for any host, not only the webspace layout it was written for.
Three optional keys in the env file name what differs (see the script's header): `WERKATOR_REPO_DIR` (directory of the watched repository, which also names the systemd unit), `WERKATOR_INSTALL_DIR` (where the runtime bundle is unpacked), and `WERKATOR_SANDBOX` (`bwrap`, the default, or `docker` — a Docker host has no werkdock binary and no rootfs archive to upload).
Three optional keys in the env file name what differs (see the script's header): `WERKATOR_REPO_DIR` (directory of the watched repository, which also names the systemd unit), `WERKATOR_INSTALL_DIR` (where the runtime bundle is unpacked), and `WERKATOR_SANDBOX` (`werkdock`, the default, or `docker` — a Docker host has no werkdock binary and no rootfs archive to upload).
Their defaults are the layout `instance-install` creates, so an env file that names none of them behaves exactly as before.
The upload happens before the service is stopped and every artifact is checksum-verified after the transfer, so a dropped connection costs the transfer and not the running service.
@@ -329,7 +329,7 @@ Werkator runs as a systemd *user* service on the assigned localhost port ("eigen
Werkator is never built on the webspace: the runtime bundle and the werkdock binary are built locally and uploaded (ADR 0006).
All steps are driven by `tools/remote`; commands name their role — `instance-*` manages the installed Werkator, `repo-*` the repository it watches.
Each instance is a pair of files (step 23): a transport env file selected with `--env-file` (default `.env`), and a YAML fragment in the configuration schema, named by its `WERKATOR_INIT_CONFIG` key and installed remotely via `werkator init --apply` — e.g. `.env.mih34` + `.env.mih34.yml`, both gitignored.
The fragment carries the Werkator configuration (`server.port`, `publicBaseUrl`, systemd limits, `builds.default.bwrap.*`); the env file only says where and how to reach the host.
The fragment carries the Werkator configuration (`server.port`, `publicBaseUrl`, systemd limits, `builds.default.werkdock.*`); the env file only says where and how to reach the host.
```bash
tools/remote --env-file .env.mih34 werkator check-prerequisites # uploads werkdock, runs its doctor