Files
werkator/docs/plan/11-docker-build-runtime.md
mhoennigandClaude Opus 5 35f06ec1ec 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>
2026-08-30 19:39:55 +02:00

6.2 KiB

Step 11: Docker Build Runtime (optional)

Prerequisites: step 04. Read README.md and 00-legacy-analysis.md first. This step is optional; skip it until builds actually need container isolation.

Goal

Run build commands inside a Docker container, as the legacy --docker mode did.

Design

Implement a DockerBuildRunner for the BuildRunner interface from step 04, shelling out to the docker CLI (consistent with the git gateway approach; no Docker Java SDK dependency).

Port from legacy (see analysis, lines ~4400+):

  • Ensure image: build from configured Dockerfile/context when missing or stale; track staleness via an image label holding the SHA-256 of Dockerfile + context (legacy org.werkator.build-inputs-sha256).
  • Gradle cache volume per repository (werkator-gradle-<repo-key>), mounted and chowned to the host UID/GID.
  • Run the build container: workspace mount, branch env var, configured extra env, network mode, docker socket mount for Testcontainers-based builds.
  • Post-build ownership repair of the workspace (legacy repair_docker_workspace_ownership).
  • Label all containers (org.hoennig.werkator=true, repository, role) and clean up stale ones on startup.

Decide during implementation whether the hsadmin-ng-specific legacy options (preflight command, JAVA_TOOL_OPTIONS injection) are needed; default to NOT porting them (see orphaned-config list in the analysis).

Config

New branches.<name>.docker section: enabled, image, dockerfile, context, network, env. Update WerkatorConfig, InitCommand templates, and docs/configuration.md together.

Tests

  • Unit tests for command assembly (assert the exact docker run/docker build argv) with a mocked command runner.
  • Checksum staleness logic.
  • Optional: one Testcontainers-gated integration test that runs a trivial build in a stock image; skip when Docker is unavailable.

Acceptance Criteria

  • ./gradlew ktlintFormat then ./gradlew build is green with Docker absent.
  • Native execution path (step 04) is unchanged and remains the default.

Implementation Notes (2026-07-07)

Implemented as designed: DockerBuildRunner (in build/) implements BuildRunner and shells out to the docker CLI via the generic GitCommandRunner process wrapper. The runtime is selected per branch by DispatchingBuildRunner (@Primary), so BuildExecutor keeps a single BuildRunner dependency and the native ProcessBuildRunner stays the default. No test needs Docker; the main docker run argv is asserted exactly through an injectable process launcher, everything else through the mocked command runner.

Ported from legacy:

  • Image ensure (ensure_docker_build_image): rebuild when the image is missing or the org.werkator.build-inputs-sha256 label no longer matches; all four org.werkator.* labels are set. Without a configured dockerfile, the image is used as-is and pulled by docker run on demand.
  • Gradle cache volume werkator-gradle-<repo-key>, created and chowned to the host uid/gid with the legacy container script.
  • Build container: workspace bind mount, branch env var, configured extra env, network mode, docker socket mount with DOCKER_HOST/TESTCONTAINERS_* for Testcontainers-based builds, --add-host host.docker.internal:host-gateway off host network.
  • Ownership repair of build/ and .gradle/ (repair_docker_workspace_ownership).
  • org.hoennig.werkator labels (role build) and stale-container cleanup.

Deviations and decisions:

  • The BuildRunner interface gained repoDir and branchConfig parameters (with defaults), because runner selection and Docker settings are per branch and the per-repo volume/container names need the repository path — a worktree cannot resolve the uncommitted config layer.
  • The hsadmin-ng-specific legacy options were not ported, as the step suggests: no preflight command, no JAVA_TOOL_OPTIONS injection, no .testcontainers.properties generation, no HSADMINNG_* env passthrough — TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE/TESTCONTAINERS_HOST_OVERRIDE/DOCKER_HOST cover modern Testcontainers; anything else fits docker.env.
  • Ownership repair runs inside the same build container (wrapped around the command, preserving its exit code) instead of a follow-up root container; the separate prepare_docker_workspace_build_dir step became unnecessary because the clean command already runs in the container.
  • Container names are per branch (werkator-build-<repo-key>-<branch-key>), not per repository, because builds of different branches may run concurrently.
  • Containers run with --init, so termination signals from build cancellation reach the build process inside the container.
  • Stale labelled containers are removed before the first Docker build of the process, not at daemon startup, so installations that never build in Docker never invoke docker.
  • The Gradle volume is prepared once per process and image, not before every build.
  • A missing unix socket skips the socket mount instead of failing the build (legacy errored); a tcp:// DOCKER_HOST also skips it.
  • docker.network defaults to Docker's default network, not to host like legacy (host mode was an hsadmin-ng-ism); network: host switches the Testcontainers host override to localhost exactly like legacy.
  • Image-input checksums are computed in-process (DockerImageInputs), not via sha256sum, with the same input format as legacy.

Manual smoke test (2026-07-07, scratch repo, Rancher Desktop 27.3.1):

  • A scratch repo with docker.enabled, a two-line Dockerfile, and a build command writing id -u into build/who.txt: werkator build built the image with all four labels, created the werkator-gradle-<repo-key> volume, streamed the container output live, and exited 0 (success after 0:14).
  • A second run reused the image (inputs label matched, no rebuild; success after 0:04).
  • The command ran as uid 0 inside the container while build/who.txt ended up owned by the host user — the in-container ownership repair works.
  • No labelled containers were left behind after the builds.
  • Caveat found while testing (environmental, not 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.