From 35f06ec1ec185c2732ef2d2b92378b862db67693 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sun, 30 Aug 2026 19:39:55 +0200 Subject: [PATCH] Rename GitTally to Werkator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .claude/skills/architecture/SKILL.md | 20 +-- .claude/skills/writing-tests/SKILL.md | 4 +- .gitignore | 2 +- .idea/{gittally.iml => werkator.iml} | 0 .gittally.yml => .werkator.yml | 2 +- AGENTS.md | 10 +- CLAUDE.md | 2 +- README.md | 7 +- docs/adrs/0001-2026-06-09.test-framework.md | 2 +- docs/adrs/0002-2026-06-09.gradle-version.md | 2 +- .../0003-2026-06-09.spring-boot-version.md | 2 +- .../0004-2026-07-07.rewrite-architecture.md | 2 +- .../adrs/0005-2026-07-07.managed-nginx-tls.md | 14 +- ...-2026-08-10.runtime-bundle-distribution.md | 14 +- .../adrs/0007-2026-08-28.build-definitions.md | 2 +- docs/bootstrapping.md | 18 +-- docs/configuration.md | 53 ++++--- docs/deployment.md | 26 ++-- ...selfhost.sh => setup-werkator-selfhost.sh} | 12 +- ...server.sh => setup-werkator-testserver.sh} | 8 +- docs/migration-from-legacy.md | 94 ------------ docs/plan/00-legacy-analysis.md | 10 +- docs/plan/03-gitea-client.md | 2 +- docs/plan/08-web-ui.md | 4 +- docs/plan/10-cli-commands.md | 4 +- docs/plan/11-docker-build-runtime.md | 2 +- docs/plan/12-deployment.md | 3 +- docs/plan/13-nginx-tls.md | 6 +- docs/plan/15-runtime-bundle-distribution.md | 20 +-- docs/plan/16-git-in-docker-builds.md | 4 +- docs/plan/17-bwrap-build-runtime.md | 26 ++-- docs/plan/18-remove-branches-section.md | 3 +- docs/plan/19-watcher-health-in-ui.md | 2 +- docs/plan/README.md | 6 +- docs/werkator-migrationsplan.md | 145 ++++++++++++++++++ packaging/{gittally => werkator} | 2 +- .../{gittally => werkator}/SecretFiles.kt | 0 .../WerkatorApplication.kt | 4 +- .../{gittally => werkator}/WerkatorCommand.kt | 4 +- .../artifacts/ArtifactsConfiguration.kt | 0 .../artifacts/FileArtifactStore.kt | 2 +- .../build/ArtifactKeys.kt | 0 .../build/ArtifactStore.kt | 0 .../build/BranchWorkspaces.kt | 0 .../build/BuildConfiguration.kt | 0 .../build/BuildExecutor.kt | 0 .../build/BuildResult.kt | 0 .../build/BuildResultRepository.kt | 0 .../build/BuildRunner.kt | 0 .../build/BuildStatus.kt | 0 .../build/DockerBuildRunner.kt | 16 +- .../build/DockerImageInputs.kt | 0 .../build/DockerSocketLocator.kt | 0 .../build/FileBuildResultRepository.kt | 0 .../build/RunningBuild.kt | 0 .../commands/BranchNameResolution.kt | 0 .../commands/BuildCommand.kt | 0 .../commands/ConfigPrintCommand.kt | 0 .../commands/ConsoleBuildRunner.kt | 0 .../commands/InitCommand.kt | 16 +- .../commands/RetryCommand.kt | 0 .../commands/ServerCommand.kt | 4 +- .../commands/StatusCommand.kt | 0 .../commands/SystemdServiceFiles.kt | 14 +- .../config/BuildDefinition.kt | 0 .../de/hoennig/werkator/config/ConfigFiles.kt | 47 ++++++ .../config/ConfigLoader.kt | 19 ++- .../config/ConfigVersion.kt | 34 ++-- .../config/DurationParser.kt | 0 .../config/WerkatorConfig.kt | 6 +- .../{gittally => werkator}/git/GitAskPass.kt | 8 +- .../git/GitCommandRunner.kt | 0 .../{gittally => werkator}/git/GitService.kt | 0 .../gitea/GiteaClient.kt | 0 .../gitea/GiteaStateMapping.kt | 0 .../metrics/MetricsConfiguration.kt | 0 .../metrics/SystemMetrics.kt | 0 .../metrics/SystemMetricsCollector.kt | 0 .../{gittally => werkator}/server/ApiDtos.kt | 0 .../server/ArtifactFileController.kt | 0 .../server/BranchListing.kt | 0 .../server/BranchPermalinks.kt | 0 .../server/BuildsApiController.kt | 0 .../server/ControlTokenService.kt | 0 .../server/NginxConfigFiles.kt | 2 +- .../server/NginxProxyManager.kt | 22 +-- .../server/ServerConfiguration.kt | 0 .../server/ServerMetricsLifecycle.kt | 0 .../server/ServerNginxLifecycle.kt | 0 .../server/ServerWatcherLifecycle.kt | 0 .../server/StatusApiController.kt | 0 .../server/SystemApiController.kt | 0 .../server/UiController.kt | 4 +- .../{gittally => werkator}/server/UiViews.kt | 0 .../server/WatcherApiController.kt | 0 .../watcher/AutoBuildState.kt | 0 .../{gittally => werkator}/watcher/Watcher.kt | 9 +- .../watcher/WatcherConfiguration.kt | 0 .../watcher/WatcherState.kt | 0 src/main/resources/static/favicon.svg | 2 +- .../static/{gittally.css => werkator.css} | 2 +- .../static/{gittally.js => werkator.js} | 4 +- src/main/resources/templates/fragments.html | 4 +- src/main/resources/templates/releases.html | 30 +++- .../ApplicationContextTest.kt | 2 +- .../ServerModeApplicationTest.kt | 0 .../BuildExecutorArtifactIntegrationTest.kt | 0 .../artifacts/FileArtifactStoreTest.kt | 0 .../build/ArtifactKeysTest.kt | 0 .../build/BuildExecutorTest.kt | 0 .../build/BuildStatusTest.kt | 0 .../build/DispatchingBuildRunnerTest.kt | 0 .../build/DockerBuildRunnerTest.kt | 0 .../build/DockerImageInputsTest.kt | 0 .../build/FileBuildResultRepositoryTest.kt | 0 .../build/GitWorktreeWorkspacesTest.kt | 4 +- .../build/ProcessBuildRunnerTest.kt | 0 .../commands/BranchNameResolutionTest.kt | 0 .../commands/BuildCommandTest.kt | 0 .../commands/CapturedConsole.kt | 0 .../commands/ConfigPrintCommandTest.kt | 0 .../commands/ConsoleBuildRunnerTest.kt | 0 .../commands/InitCommandTest.kt | 0 .../commands/RetryCommandTest.kt | 0 .../commands/StatusCommandTest.kt | 0 .../commands/SystemdServiceFilesTest.kt | 2 +- .../config/ConfigLoaderTest.kt | 54 ++++++- .../config/ConfigVersionsTest.kt | 6 +- .../config/DurationParserTest.kt | 0 .../framework/MockKSmokeTest.kt | 0 .../framework/SpringMockKSmokeTest.kt | 0 .../framework/TestcontainersSmokeTest.kt | 0 .../framework/WireMockSmokeTest.kt | 0 .../git/GitAskPassTest.kt | 0 .../git/GitCommandRunnerTest.kt | 2 +- .../git/GitServiceTest.kt | 4 +- .../gitea/GiteaClientTest.kt | 2 +- .../gitea/GiteaStateMappingTest.kt | 0 .../metrics/SystemMetricsCollectorTest.kt | 0 .../server/ArtifactFileControllerTest.kt | 0 .../server/BranchListingTest.kt | 0 .../server/BranchPermalinksTest.kt | 0 .../server/BuildsApiControllerTest.kt | 0 .../server/ControlTokenServiceTest.kt | 0 .../server/NginxConfigFilesTest.kt | 0 .../server/NginxProxyManagerTest.kt | 2 +- .../server/PermanentBranchRoutesTest.kt | 0 .../server/ServerNginxLifecycleTest.kt | 0 .../server/StatusApiControllerTest.kt | 0 .../server/SystemApiControllerTest.kt | 0 .../server/UiControllerTest.kt | 2 +- .../server/UiViewsTest.kt | 0 .../server/WatcherApiControllerTest.kt | 0 .../watcher/AutoBuildStateTest.kt | 0 .../watcher/WatcherTest.kt | 19 ++- ...tally-instance => setup-werkator-instance} | 118 +++++++------- 156 files changed, 604 insertions(+), 401 deletions(-) rename .idea/{gittally.iml => werkator.iml} (100%) rename .gittally.yml => .werkator.yml (95%) rename docs/examples/{setup-gittally-selfhost.sh => setup-werkator-selfhost.sh} (85%) rename docs/examples/{setup-gittally-testserver.sh => setup-werkator-testserver.sh} (95%) delete mode 100644 docs/migration-from-legacy.md create mode 100644 docs/werkator-migrationsplan.md rename packaging/{gittally => werkator} (82%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/SecretFiles.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/WerkatorApplication.kt (94%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/WerkatorCommand.kt (94%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/artifacts/ArtifactsConfiguration.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/artifacts/FileArtifactStore.kt (99%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/ArtifactKeys.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/ArtifactStore.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BranchWorkspaces.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildConfiguration.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildExecutor.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildResult.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildResultRepository.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildRunner.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/BuildStatus.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/DockerBuildRunner.kt (97%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/DockerImageInputs.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/DockerSocketLocator.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/FileBuildResultRepository.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/build/RunningBuild.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/BranchNameResolution.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/BuildCommand.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/ConfigPrintCommand.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/ConsoleBuildRunner.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/InitCommand.kt (97%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/RetryCommand.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/ServerCommand.kt (96%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/StatusCommand.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/commands/SystemdServiceFiles.kt (90%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/config/BuildDefinition.kt (100%) create mode 100644 src/main/kotlin/de/hoennig/werkator/config/ConfigFiles.kt rename src/main/kotlin/de/hoennig/{gittally => werkator}/config/ConfigLoader.kt (96%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/config/ConfigVersion.kt (81%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/config/DurationParser.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/config/WerkatorConfig.kt (97%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/git/GitAskPass.kt (85%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/git/GitCommandRunner.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/git/GitService.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/gitea/GiteaClient.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/gitea/GiteaStateMapping.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/metrics/MetricsConfiguration.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/metrics/SystemMetrics.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/metrics/SystemMetricsCollector.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ApiDtos.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ArtifactFileController.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/BranchListing.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/BranchPermalinks.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/BuildsApiController.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ControlTokenService.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/NginxConfigFiles.kt (98%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/NginxProxyManager.kt (96%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ServerConfiguration.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ServerMetricsLifecycle.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ServerNginxLifecycle.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/ServerWatcherLifecycle.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/StatusApiController.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/SystemApiController.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/UiController.kt (99%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/UiViews.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/server/WatcherApiController.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/watcher/AutoBuildState.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/watcher/Watcher.kt (98%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/watcher/WatcherConfiguration.kt (100%) rename src/main/kotlin/de/hoennig/{gittally => werkator}/watcher/WatcherState.kt (100%) rename src/main/resources/static/{gittally.css => werkator.css} (99%) rename src/main/resources/static/{gittally.js => werkator.js} (99%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/ApplicationContextTest.kt (95%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/ServerModeApplicationTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/artifacts/BuildExecutorArtifactIntegrationTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/artifacts/FileArtifactStoreTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/ArtifactKeysTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/BuildExecutorTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/BuildStatusTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/DispatchingBuildRunnerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/DockerBuildRunnerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/DockerImageInputsTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/FileBuildResultRepositoryTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/GitWorktreeWorkspacesTest.kt (98%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/build/ProcessBuildRunnerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/BranchNameResolutionTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/BuildCommandTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/CapturedConsole.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/ConfigPrintCommandTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/ConsoleBuildRunnerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/InitCommandTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/RetryCommandTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/StatusCommandTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/commands/SystemdServiceFilesTest.kt (98%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/config/ConfigLoaderTest.kt (92%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/config/ConfigVersionsTest.kt (95%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/config/DurationParserTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/framework/MockKSmokeTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/framework/SpringMockKSmokeTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/framework/TestcontainersSmokeTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/framework/WireMockSmokeTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/git/GitAskPassTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/git/GitCommandRunnerTest.kt (97%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/git/GitServiceTest.kt (99%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/gitea/GiteaClientTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/gitea/GiteaStateMappingTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/metrics/SystemMetricsCollectorTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/ArtifactFileControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/BranchListingTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/BranchPermalinksTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/BuildsApiControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/ControlTokenServiceTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/NginxConfigFilesTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/NginxProxyManagerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/PermanentBranchRoutesTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/ServerNginxLifecycleTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/StatusApiControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/SystemApiControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/UiControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/UiViewsTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/server/WatcherApiControllerTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/watcher/AutoBuildStateTest.kt (100%) rename src/test/kotlin/de/hoennig/{gittally => werkator}/watcher/WatcherTest.kt (97%) rename tools/{setup-gittally-instance => setup-werkator-instance} (67%) diff --git a/.claude/skills/architecture/SKILL.md b/.claude/skills/architecture/SKILL.md index f651ba3..a2df19d 100644 --- a/.claude/skills/architecture/SKILL.md +++ b/.claude/skills/architecture/SKILL.md @@ -1,23 +1,23 @@ --- name: architecture -description: Detailed werkator subsystem architecture — CLI wiring and exit codes, server mode, web UI, configuration system, git access, build execution (native and Docker), watcher poll cycle, and system metrics. Use when designing or modifying code in the commands, config, git, gitea, build, artifacts, watcher, metrics, or server packages, or when a question goes beyond the overview in AGENTS.md. +description: Detailed Werkator subsystem architecture — CLI wiring and exit codes, server mode, web UI, configuration system, git access, build execution (native and Docker), watcher poll cycle, and system metrics. Use when designing or modifying code in the commands, config, git, gitea, build, artifacts, watcher, metrics, or server packages, or when a question goes beyond the overview in AGENTS.md. --- -# werkator Architecture +# Werkator Architecture -werkator is a lightweight, declarative CI/CD build system. +Werkator is a lightweight, declarative CI/CD build system. It is a dual-mode application: **CLI** (interactive, status, config) and **Server** (HTTP, persistent). ## Entry Point and CLI Wiring Spring Boot starts via `WerkatorApplication`. A separate `CliRunner` component (in the same file) implements both `CommandLineRunner` (runs picocli) and `ExitCodeGenerator` (returns the exit code). `exitProcess` is called only from `main()` via `SpringApplication.exit()` — **never** inside `run()`. This keeps the Spring context alive during tests. -Picocli commands are Spring `@Component` beans. The root command (`werkatorCommand`) declares subcommands as class references in `@Command(subcommands = [...])`. Picocli resolves them from the Spring context via the auto-configured `IFactory` bean. +Picocli commands are Spring `@Component` beans. The root command (`WerkatorCommand`) declares subcommands as class references in `@Command(subcommands = [...])`. Picocli resolves them from the Spring context via the auto-configured `IFactory` bean. ``` -werkatorApplication ← @SpringBootApplication +WerkatorApplication ← @SpringBootApplication CliRunner ← CommandLineRunner + ExitCodeGenerator -werkatorCommand ← root @Command, delegates to subcommands +WerkatorCommand ← root @Command, delegates to subcommands commands/ InitCommand ← "init [--systemd]" ServerCommand ← "server" @@ -40,16 +40,18 @@ Two independent staleness signals, never merged: the `live-indicator` badge says ## Configuration System -werkator is configured by two YAML files, deep-merged by `ConfigLoader` (later wins): +Werkator is configured by two YAML files, deep-merged by `ConfigLoader` (later wins): 1. `.werkator.yml` at the repo root — committed, shared team settings. 2. `.git/werkator/.werkator.yml` — not committed; machine-specific overrides and secrets (`git.account`, `git.token`). +Every lookup falls back to the pre-rename name (`ConfigFiles`): `.gittally.yml`, and `.git/gittally/.gittally.yml` for the machine layer. Current name first, and where both exist the old one is ignored rather than merged — a missing config is not an error, so an un-renamed installation would otherwise start on defaults without a single failure. + On top of those comes the **branch layer**: the `.werkator.yml` committed on a branch, applied by `loadWithBranchLayer` (the watcher passes the content read via `git show`, `loadForWorktree` the file in the build worktree). A branch describes its own CI and wins over both layers — the whole `builds` section — so a configuration can be tried out on a branch without touching other branches' builds. `stripPinned` removes what is not a description of this branch's build: `git`, `server`, `gitea`, `executor`, `watcher`, and — inside every `builds` definition as well as every legacy `branches` entry — `requirePullRequest`, `statusContext`, and `docker.enabled`/`docker.network`. -Each file is version-checked before merging (`werkator.version.since`/`below`, `ConfigVersions.verdict`), so the message can name the file to fix: `since` is hard in both directions — too old a werkator, or a file written before `ConfigVersions.FORMAT_BROKE_IN` and read after it — while `below` only warns. There is no format version (`apiVersion`) on purpose: only one configuration generation is supported, and the declared version exists to make the incompatibility nameable. +Each file is version-checked before merging (`werkator.version.since`/`below`, `ConfigVersions.verdict`), so the message can name the file to fix: `since` is hard in both directions — too old a Werkator, or a file written before `ConfigVersions.FORMAT_BROKE_IN` and read after it — while `below` only warns. There is no format version (`apiVersion`) on purpose: only one configuration generation is supported, and the declared version exists to make the incompatibility nameable. -After merging, `resolveBuildSections` decides which section describes the builds: `builds` or the legacy `branches`, never both. With no real build definition (`builds.maxConcurrent` is not one, `dropNonDefinitionBuilds` already drops it) the legacy path runs and `branches.default` is merged into every other branch entry; otherwise `branches` is dropped with a warning and `mergeBuildDefaults` applies `builds.default` as the base of every other definition — its settings only, never its `trigger` block (`TRIGGER_KEYS`, a single key so that a selector added to `TriggerConfig` later is non-inheritable by construction). `checkTriggerBlocks` refuses a definition still writing `onPush`/`atTimes`/`branches`/`activeWithin` flat, per file and scoped like the version check. Deciding this on the merged map is deliberate: a build defined on a branch and unknown to the host still inherits the host's `builds.default`, sandbox policy included, which is what keeps the pinned keys effective for it. The result is bound to the `WerkatorConfig` data classes (`config/werkatorConfig.kt`), which define the schema and all defaults; `werkatorConfig.buildSettings(branch, build)` is the single answer to "what does this build run". +After merging, `resolveBuildSections` decides which section describes the builds: `builds` or the legacy `branches`, never both. With no real build definition (`builds.maxConcurrent` is not one, `dropNonDefinitionBuilds` already drops it) the legacy path runs and `branches.default` is merged into every other branch entry; otherwise `branches` is dropped with a warning and `mergeBuildDefaults` applies `builds.default` as the base of every other definition — its settings only, never its `trigger` block (`TRIGGER_KEYS`, a single key so that a selector added to `TriggerConfig` later is non-inheritable by construction). `checkTriggerBlocks` refuses a definition still writing `onPush`/`atTimes`/`branches`/`activeWithin` flat, per file and scoped like the version check. Deciding this on the merged map is deliberate: a build defined on a branch and unknown to the host still inherits the host's `builds.default`, sandbox policy included, which is what keeps the pinned keys effective for it. The result is bound to the `WerkatorConfig` data classes (`config/WerkatorConfig.kt`), which define the schema and all defaults; `WerkatorConfig.buildSettings(branch, build)` is the single answer to "what does this build run". Three places must stay in sync when config keys change: the `WerkatorConfig` data classes, the commented templates generated by `InitCommand`, and the reference in `docs/configuration.md`. diff --git a/.claude/skills/writing-tests/SKILL.md b/.claude/skills/writing-tests/SKILL.md index b6a8bc3..9cc8e02 100644 --- a/.claude/skills/writing-tests/SKILL.md +++ b/.claude/skills/writing-tests/SKILL.md @@ -1,9 +1,9 @@ --- name: writing-tests -description: werkator testing conventions — Kotest FunSpec spec structure, MockK matchers, and the two patterns for mocking beans in Spring slice tests (springmockk @MockkBean or @TestConfiguration). Use when writing, extending, or refactoring tests. +description: Werkator testing conventions — Kotest FunSpec spec structure, MockK matchers, and the two patterns for mocking beans in Spring slice tests (springmockk @MockkBean or @TestConfiguration). Use when writing, extending, or refactoring tests. --- -# Writing Tests for werkator +# Writing Tests for Werkator Tests use **Kotest `FunSpec`** style. `SpringExtension` is registered globally in `io.kotest.provided.ProjectConfig` — do not add it per-spec. diff --git a/.gitignore b/.gitignore index cf4db1d..a081a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ # Package Files # *.jar -# ... but builds in fresh checkouts (e.g. werkator worktrees) need the wrapper +# ... but builds in fresh checkouts (e.g. Werkator worktrees) need the wrapper !gradle/wrapper/gradle-wrapper.jar *.war *.nar diff --git a/.idea/gittally.iml b/.idea/werkator.iml similarity index 100% rename from .idea/gittally.iml rename to .idea/werkator.iml diff --git a/.gittally.yml b/.werkator.yml similarity index 95% rename from .gittally.yml rename to .werkator.yml index 384a0e4..70631bd 100644 --- a/.gittally.yml +++ b/.werkator.yml @@ -1,5 +1,5 @@ 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: "" # Gitea integration for fetching commits and posting build statuses. diff --git a/AGENTS.md b/AGENTS.md index c4cb33d..d5849ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# werkator — Agent Instructions +# Werkator — Agent Instructions This file holds the shared, tool-agnostic instructions for all AI coding agents. Claude Code imports it from `CLAUDE.md` via `@AGENTS.md`; Claude-Code-specific instructions belong in `CLAUDE.md`, everything else here. @@ -24,7 +24,7 @@ java -jar build/libs/werkator.jar init ## Architecture Overview -werkator is a lightweight, declarative CI/CD build system: git-centric, one instance per repository, builds native or in Docker, statuses reported to Gitea. +Werkator is a lightweight, declarative CI/CD build system: git-centric, one instance per repository, builds native or in Docker, statuses reported to Gitea. It is a dual-mode application: **CLI** (interactive, status, config) and **Server** (HTTP, persistent web UI + JSON API). IMPORTANT: Before designing or modifying code in any production package, load the [architecture skill](.claude/skills/architecture/SKILL.md) — it holds the subsystem details (CLI wiring, server mode, web UI, config system, git access, build execution, watcher, metrics). @@ -38,7 +38,7 @@ All production code lives under `de.hoennig.werkator`, with sub-packages `comman - Nothing is scheduled during CLI runs or tests: the watcher poll loop and metrics sampling start only via an explicit `start()` in the `server` profile. - Builds run detached in worktrees under `.git/werkator/worktrees/`; the primary checkout is never used for builds; never assume a single running build. - When config keys change, three places must stay in sync: the `WerkatorConfig` data classes, the `InitCommand` templates, and `docs/configuration.md`. -- Every config file may declare `werkator.version.since`/`below` (the werkator it is written for, never a format version — no API is involved). `since` is enforced in both directions, using `ConfigVersions.FORMAT_BROKE_IN` for "file predates a breaking change"; `below` only warns. A violation aborts the start for the machine and project config, but fails only that branch's builds for a branch config. +- Every config file may declare `werkator.version.since`/`below` (the Werkator it is written for, never a format version — no API is involved). `since` is enforced in both directions, using `ConfigVersions.FORMAT_BROKE_IN` for "file predates a breaking change"; `below` only warns. A violation aborts the start for the machine and project config, but fails only that branch's builds for a branch config. - A branch describes its own CI: its committed `.werkator.yml` is the branch layer (`ConfigLoader.loadWithBranchLayer`, used by the watcher per origin branch and by `loadForWorktree` at build time) and takes precedence over `.git`/project — including the whole `builds` section, so a new configuration can be tried out on a branch without affecting other branches. Only the pinned set is stripped from that layer: secrets (`git`), host/repository sections (`server`, `gitea`, `executor`, `watcher`), the docker sandbox policy (`docker.enabled`, `docker.network`), and the trust gate (`requirePullRequest`). A branch must never reach credentials, disable its container, change its network, raise global concurrency, or bypass its own pull-request gate; a branch's definitions apply to that branch alone. - A build definition carries the complete description of its build, split in two: the `trigger` block (`onPush`, `atTimes`, `branches`, `activeWithin`) says when and for which branches it runs, everything else what it does. `builds.default` is the base every other definition inherits its settings — never its `trigger` — from. The split is structural so that a selector added to `TriggerConfig` later is non-inheritable by construction; writing a trigger key flat is refused, never ignored, because ignoring it leaves a build that silently stops running. A `!` prefix in `trigger.branches` excludes and always wins. - The inheritance is applied after all layers are merged: that order is what makes a build a branch invents inherit the host's sandbox policy instead of the data-class default, so the pinning also holds for a build the host has never heard of. Pinned are `requirePullRequest`, `statusContext`, `docker.enabled`, and `docker.network`. @@ -64,8 +64,8 @@ Keep sentences short. - `docs/Werkator-Konzept.md` — product concept and target architecture (in German): git-centric CI, builds in Docker, one instance per repository, status reported back to Gitea. - `docs/configuration.md` — configuration reference; keep in sync with `WerkatorConfig` and the `init` templates. - `docs/bootstrapping.md` — how `init` prepares a repository. -- `docs/deployment.md` — running werkator as a systemd user service behind an existing reverse proxy (`init --systemd` generates the unit). -- `docs/migration-from-legacy.md` — legacy env vars → YAML keys mapping and the manual migration steps; `legacy/werkator` is deprecated. +- `docs/deployment.md` — running Werkator as a systemd user service behind an existing reverse proxy (`init --systemd` generates the unit). +- `docs/werkator-migrationsplan.md` — renaming a running installation from GitTally to Werkator: what the name fallback covers and what has to be moved by hand. - `docs/plan/` — the step-by-step rewrite plan; `docs/plan/README.md` explains how to execute a step, `docs/plan/00-legacy-analysis.md` summarizes the legacy bash script. - `docs/prs/` — one document per pull request; every PR needs one. IMPORTANT: Before opening or finishing a pull request, load the [pr-doc skill](.claude/skills/pr-doc/SKILL.md) and write the PR-doc. diff --git a/CLAUDE.md b/CLAUDE.md index 659128b..2ad2a0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# werkator — Claude Code Instructions +# Werkator — Claude Code Instructions @AGENTS.md diff --git a/README.md b/README.md index fcf5659..12da1cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# werkator +# Werkator Lightweight, declarative and highly opinionated software build system (CI/CD). @@ -6,13 +6,12 @@ Lightweight, declarative and highly opinionated software build system (CI/CD). - [docs/configuration.md](docs/configuration.md) — configuration reference - [docs/bootstrapping.md](docs/bootstrapping.md) — initializing a repository with `init` -- [docs/deployment.md](docs/deployment.md) — running werkator as a systemd service behind a reverse proxy -- [docs/migration-from-legacy.md](docs/migration-from-legacy.md) — migrating from the legacy bash script +- [docs/deployment.md](docs/deployment.md) — running Werkator as a systemd service behind a reverse proxy ## Legacy Script `legacy/werkator` (bash) is **deprecated** and kept only as a behavioral reference for the rewrite. -Do not use it for new installations; see [docs/migration-from-legacy.md](docs/migration-from-legacy.md). +Do not use it for new installations. ## Developer Setup diff --git a/docs/adrs/0001-2026-06-09.test-framework.md b/docs/adrs/0001-2026-06-09.test-framework.md index acc228e..b8a7c12 100644 --- a/docs/adrs/0001-2026-06-09.test-framework.md +++ b/docs/adrs/0001-2026-06-09.test-framework.md @@ -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. diff --git a/docs/adrs/0002-2026-06-09.gradle-version.md b/docs/adrs/0002-2026-06-09.gradle-version.md index 05677ef..5e281a6 100644 --- a/docs/adrs/0002-2026-06-09.gradle-version.md +++ b/docs/adrs/0002-2026-06-09.gradle-version.md @@ -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. diff --git a/docs/adrs/0003-2026-06-09.spring-boot-version.md b/docs/adrs/0003-2026-06-09.spring-boot-version.md index c7d86e5..8a336f8 100644 --- a/docs/adrs/0003-2026-06-09.spring-boot-version.md +++ b/docs/adrs/0003-2026-06-09.spring-boot-version.md @@ -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, diff --git a/docs/adrs/0004-2026-07-07.rewrite-architecture.md b/docs/adrs/0004-2026-07-07.rewrite-architecture.md index ae7ad5f..f1adc2f 100644 --- a/docs/adrs/0004-2026-07-07.rewrite-architecture.md +++ b/docs/adrs/0004-2026-07-07.rewrite-architecture.md @@ -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 diff --git a/docs/adrs/0005-2026-07-07.managed-nginx-tls.md b/docs/adrs/0005-2026-07-07.managed-nginx-tls.md index c446bf2..77b9fc2 100644 --- a/docs/adrs/0005-2026-07-07.managed-nginx-tls.md +++ b/docs/adrs/0005-2026-07-07.managed-nginx-tls.md @@ -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 diff --git a/docs/adrs/0006-2026-08-10.runtime-bundle-distribution.md b/docs/adrs/0006-2026-08-10.runtime-bundle-distribution.md index f0955d6..d16f7c7 100644 --- a/docs/adrs/0006-2026-08-10.runtime-bundle-distribution.md +++ b/docs/adrs/0006-2026-08-10.runtime-bundle-distribution.md @@ -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. diff --git a/docs/adrs/0007-2026-08-28.build-definitions.md b/docs/adrs/0007-2026-08-28.build-definitions.md index 381f510..ddfb80c 100644 --- a/docs/adrs/0007-2026-08-28.build-definitions.md +++ b/docs/adrs/0007-2026-08-28.build-definitions.md @@ -11,7 +11,7 @@ ## Context and Problem Statement -werkator's configuration is branch-centric: `branches.` holds the build settings, and the nightly schedule (`autoBuild`) hangs off the branch. +Werkator's configuration is branch-centric: `branches.` 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. diff --git a/docs/bootstrapping.md b/docs/bootstrapping.md index 656a71a..14aa526 100644 --- a/docs/bootstrapping.md +++ b/docs/bootstrapping.md @@ -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 /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. diff --git a/docs/configuration.md b/docs/configuration.md index 22936ab..8018f45 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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:///`. @@ -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/` points at — but build tools do. +Werkator itself never needs those refs to be current — it builds the commit `refs/remotes/origin/` 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..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-` 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) diff --git a/docs/deployment.md b/docs/deployment.md index d9fdc94..20df304 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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. diff --git a/docs/examples/setup-gittally-selfhost.sh b/docs/examples/setup-werkator-selfhost.sh similarity index 85% rename from docs/examples/setup-gittally-selfhost.sh rename to docs/examples/setup-werkator-selfhost.sh index 76a5783..d1be0e7 100755 --- a/docs/examples/setup-gittally-selfhost.sh +++ b/docs/examples/setup-werkator-selfhost.sh @@ -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 diff --git a/docs/examples/setup-gittally-testserver.sh b/docs/examples/setup-werkator-testserver.sh similarity index 95% rename from docs/examples/setup-gittally-testserver.sh rename to docs/examples/setup-werkator-testserver.sh index 9c18ce9..635179a 100755 --- a/docs/examples/setup-gittally-testserver.sh +++ b/docs/examples/setup-werkator-testserver.sh @@ -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 diff --git a/docs/migration-from-legacy.md b/docs/migration-from-legacy.md deleted file mode 100644 index b4f5ba2..0000000 --- a/docs/migration-from-legacy.md +++ /dev/null @@ -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.`; 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..buildCommand` | -| `werkator_BUILD_CLEAN_COMMAND` | `builds..cleanCommand` | -| `werkator_BUILD_ARTEFACT_DIRS` | `builds..artifactDirs` — YAML list instead of `;`-separated | -| `werkator_BUILD_STDOUT_LOG` | `builds..stdoutLog` | -| `werkator_BUILD_STDERR_LOG` | `builds..stderrLog` | -| `werkator_NEW_BRANCH_COMMIT_MAX_AGE` | `watcher.newBranchMaxAge` | -| `werkator_BUILD_DOCKER_IMAGE` | `builds..docker.image` — also set `docker.enabled: true` (replaces the `--docker` flag) | -| `werkator_BUILD_DOCKERFILE` | `builds..docker.dockerfile` | -| `werkator_BUILD_DOCKER_CONTEXT` | `builds..docker.context` | -| `werkator_BUILD_DOCKER_NETWORK` | `builds..docker.network` — default is now Docker's default network, not `host` | -| `werkator_BUILD_DOCKER_ENV` | `builds..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..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..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":"","description":"superseded by "}' \ - "$GITEA/api/v1/repos///statuses/" -``` - -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. diff --git a/docs/plan/00-legacy-analysis.md b/docs/plan/00-legacy-analysis.md index 3bd157e..66d458b 100644 --- a/docs/plan/00-legacy-analysis.md +++ b/docs/plan/00-legacy-analysis.md @@ -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. diff --git a/docs/plan/03-gitea-client.md b/docs/plan/03-gitea-client.md index 9fb78c7..6e797e1 100644 --- a/docs/plan/03-gitea-client.md +++ b/docs/plan/03-gitea-client.md @@ -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 diff --git a/docs/plan/08-web-ui.md b/docs/plan/08-web-ui.md index f9117dc..7ff778c 100644 --- a/docs/plan/08-web-ui.md +++ b/docs/plan/08-web-ui.md @@ -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. diff --git a/docs/plan/10-cli-commands.md b/docs/plan/10-cli-commands.md index a93c05f..b6ab100 100644 --- a/docs/plan/10-cli-commands.md +++ b/docs/plan/10-cli-commands.md @@ -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`, 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): diff --git a/docs/plan/11-docker-build-runtime.md b/docs/plan/11-docker-build-runtime.md index cf03604..d494c47 100644 --- a/docs/plan/11-docker-build-runtime.md +++ b/docs/plan/11-docker-build-runtime.md @@ -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. diff --git a/docs/plan/12-deployment.md b/docs/plan/12-deployment.md index 5c89bd6..3676b80 100644 --- a/docs/plan/12-deployment.md +++ b/docs/plan/12-deployment.md @@ -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: diff --git a/docs/plan/13-nginx-tls.md b/docs/plan/13-nginx-tls.md index 9b89abf..c86f7ea 100644 --- a/docs/plan/13-nginx-tls.md +++ b/docs/plan/13-nginx-tls.md @@ -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) diff --git a/docs/plan/15-runtime-bundle-distribution.md b/docs/plan/15-runtime-bundle-distribution.md index 514f926..4bf06b6 100644 --- a/docs/plan/15-runtime-bundle-distribution.md +++ b/docs/plan/15-runtime-bundle-distribution.md @@ -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_OPTS -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//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). diff --git a/docs/plan/16-git-in-docker-builds.md b/docs/plan/16-git-in-docker-builds.md index e073530..4cc92f6 100644 --- a/docs/plan/16-git-in-docker-builds.md +++ b/docs/plan/16-git-in-docker-builds.md @@ -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/` → 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 diff --git a/docs/plan/17-bwrap-build-runtime.md b/docs/plan/17-bwrap-build-runtime.md index 9a0ca9d..0cd22fe 100644 --- a/docs/plan/17-bwrap-build-runtime.md +++ b/docs/plan/17-bwrap-build-runtime.md @@ -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//rootfs` — **not** into the working tree. +Werkator unpacks it on demand (`tar --no-same-owner`) into `.git/werkator/buildenv//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; `` 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-.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//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//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:%{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. diff --git a/docs/plan/18-remove-branches-section.md b/docs/plan/18-remove-branches-section.md index b29524b..5fb4696 100644 --- a/docs/plan/18-remove-branches-section.md +++ b/docs/plan/18-remove-branches-section.md @@ -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.`; re-check it reads correctly without the legacy section existing. ## Production diff --git a/docs/plan/19-watcher-health-in-ui.md b/docs/plan/19-watcher-health-in-ui.md index ead4d13..6c5cefd 100644 --- a/docs/plan/19-watcher-health-in-ui.md +++ b/docs/plan/19-watcher-health-in-ui.md @@ -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. diff --git a/docs/plan/README.md b/docs/plan/README.md index 17e1f4f..ce4c15a 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -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. diff --git a/docs/werkator-migrationsplan.md b/docs/werkator-migrationsplan.md new file mode 100644 index 0000000..6ca944e --- /dev/null +++ b/docs/werkator-migrationsplan.md @@ -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 | `:.werkator.yml` | `:.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//` — 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//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/`, in practice `~/.local/state/werkator/artifacts/`. +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-.service` | `gittally-.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--` | `gittally-build-…` | +| Gradle cache volume | `werkator-gradle-` | `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-` 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-.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. diff --git a/packaging/gittally b/packaging/werkator similarity index 82% rename from packaging/gittally rename to packaging/werkator index 3fb290f..256337f 100755 --- a/packaging/gittally +++ b/packaging/werkator @@ -1,5 +1,5 @@ #!/bin/sh -# Launcher for the self-contained werkator runtime bundle (jlink JRE + jar). +# Launcher for the self-contained Werkator runtime bundle (jlink JRE + jar). # Built by `./gradlew runtimeBundle`; see docs/deployment.md. DIR=$(CDPATH='' cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd) # shellcheck disable=SC2086 # JAVA_OPTS is intentionally word-split diff --git a/src/main/kotlin/de/hoennig/gittally/SecretFiles.kt b/src/main/kotlin/de/hoennig/werkator/SecretFiles.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/SecretFiles.kt rename to src/main/kotlin/de/hoennig/werkator/SecretFiles.kt diff --git a/src/main/kotlin/de/hoennig/gittally/WerkatorApplication.kt b/src/main/kotlin/de/hoennig/werkator/WerkatorApplication.kt similarity index 94% rename from src/main/kotlin/de/hoennig/gittally/WerkatorApplication.kt rename to src/main/kotlin/de/hoennig/werkator/WerkatorApplication.kt index dc05b70..99e5aa9 100644 --- a/src/main/kotlin/de/hoennig/gittally/WerkatorApplication.kt +++ b/src/main/kotlin/de/hoennig/werkator/WerkatorApplication.kt @@ -20,7 +20,7 @@ class WerkatorApplication @Profile("!server") class CliRunner( private val factory: IFactory, - private val rootCommand: werkatorCommand, + private val rootCommand: WerkatorCommand, ) : CommandLineRunner, ExitCodeGenerator { private var exitCode = 0 @@ -29,7 +29,7 @@ class CliRunner( exitCode = CommandLine(rootCommand, factory) .setExecutionExceptionHandler { exception, commandLine, _ -> - // a config werkator must not read is a stated fact, not a crash: the message + // a config Werkator must not read is a stated fact, not a crash: the message // names the file, the versions, and the way out — a stack trace would bury it if (exception is ConfigException) { commandLine.err.println("Error: ${exception.message}") diff --git a/src/main/kotlin/de/hoennig/gittally/WerkatorCommand.kt b/src/main/kotlin/de/hoennig/werkator/WerkatorCommand.kt similarity index 94% rename from src/main/kotlin/de/hoennig/gittally/WerkatorCommand.kt rename to src/main/kotlin/de/hoennig/werkator/WerkatorCommand.kt index 4a9f27b..e9409c6 100644 --- a/src/main/kotlin/de/hoennig/gittally/WerkatorCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/WerkatorCommand.kt @@ -27,7 +27,7 @@ import picocli.CommandLine.Command versionProvider = BuildPropertiesVersionProvider::class, description = ["Lightweight, declarative CI/CD system"], ) -class werkatorCommand : Runnable { +class WerkatorCommand : Runnable { override fun run(): Unit = throw CommandLine.ParameterException(CommandLine(this), "Specify a subcommand") } @@ -41,5 +41,5 @@ class werkatorCommand : Runnable { class BuildPropertiesVersionProvider( private val buildProperties: ObjectProvider, ) : CommandLine.IVersionProvider { - override fun getVersion(): Array = arrayOf("werkator v${buildProperties.getIfAvailable()?.version ?: "dev"}") + override fun getVersion(): Array = arrayOf("Werkator v${buildProperties.getIfAvailable()?.version ?: "dev"}") } diff --git a/src/main/kotlin/de/hoennig/gittally/artifacts/ArtifactsConfiguration.kt b/src/main/kotlin/de/hoennig/werkator/artifacts/ArtifactsConfiguration.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/artifacts/ArtifactsConfiguration.kt rename to src/main/kotlin/de/hoennig/werkator/artifacts/ArtifactsConfiguration.kt diff --git a/src/main/kotlin/de/hoennig/gittally/artifacts/FileArtifactStore.kt b/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt similarity index 99% rename from src/main/kotlin/de/hoennig/gittally/artifacts/FileArtifactStore.kt rename to src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt index c1de69a..1ac2129 100644 --- a/src/main/kotlin/de/hoennig/gittally/artifacts/FileArtifactStore.kt +++ b/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt @@ -162,7 +162,7 @@ class FileArtifactStore( /** * The settings [build] ran with, from the build [workspace]'s `.werkator.yml` layered * on top of the primary config (see [ConfigLoader.loadForWorktree]) — resolved through - * [werkatorConfig.buildSettings], so a job's own `artifactDirs` are archived and not + * [WerkatorConfig.buildSettings], so a job's own `artifactDirs` are archived and not * only the ones its branch would have used. */ private fun buildSettings( diff --git a/src/main/kotlin/de/hoennig/gittally/build/ArtifactKeys.kt b/src/main/kotlin/de/hoennig/werkator/build/ArtifactKeys.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/ArtifactKeys.kt rename to src/main/kotlin/de/hoennig/werkator/build/ArtifactKeys.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/ArtifactStore.kt b/src/main/kotlin/de/hoennig/werkator/build/ArtifactStore.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/ArtifactStore.kt rename to src/main/kotlin/de/hoennig/werkator/build/ArtifactStore.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BranchWorkspaces.kt b/src/main/kotlin/de/hoennig/werkator/build/BranchWorkspaces.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BranchWorkspaces.kt rename to src/main/kotlin/de/hoennig/werkator/build/BranchWorkspaces.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildConfiguration.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildConfiguration.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildConfiguration.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildConfiguration.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildExecutor.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildExecutor.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildResult.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildResult.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildResult.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildResult.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildResultRepository.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildResultRepository.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildResultRepository.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildResultRepository.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildRunner.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildRunner.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildRunner.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildRunner.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/BuildStatus.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildStatus.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/BuildStatus.kt rename to src/main/kotlin/de/hoennig/werkator/build/BuildStatus.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/DockerBuildRunner.kt b/src/main/kotlin/de/hoennig/werkator/build/DockerBuildRunner.kt similarity index 97% rename from src/main/kotlin/de/hoennig/gittally/build/DockerBuildRunner.kt rename to src/main/kotlin/de/hoennig/werkator/build/DockerBuildRunner.kt index cb597b3..c93e558 100644 --- a/src/main/kotlin/de/hoennig/gittally/build/DockerBuildRunner.kt +++ b/src/main/kotlin/de/hoennig/werkator/build/DockerBuildRunner.kt @@ -194,11 +194,11 @@ class DockerBuildRunner( "ps", "-aq", "--filter", - "label=$werkator_LABEL=true", + "label=$WERKATOR_LABEL=true", "--filter", - "label=$werkator_LABEL.repository=$repoKey", + "label=$WERKATOR_LABEL.repository=$repoKey", "--filter", - "label=$werkator_LABEL.role=build", + "label=$WERKATOR_LABEL.role=build", ), repoDir, ) @@ -238,11 +238,11 @@ class DockerBuildRunner( args += listOf( "--label", - "$werkator_LABEL=true", + "$WERKATOR_LABEL=true", "--label", - "$werkator_LABEL.repository=$repoKey", + "$WERKATOR_LABEL.repository=$repoKey", "--label", - "$werkator_LABEL.role=build", + "$WERKATOR_LABEL.role=build", ) args += listOf("--workdir", "$workspace", "--volume", "$workspace:$workspace") args += gitMetadataMounts(workspace, repoDir) @@ -279,7 +279,7 @@ class DockerBuildRunner( } /** - * Makes git work inside the build container without exposing werkator's secrets. + * Makes git work inside the build container without exposing Werkator's secrets. * * The workspace is a git worktree whose `.git` file points into the primary * repository's `.git`, which is not part of the workspace mount — so any git call @@ -327,7 +327,7 @@ class DockerBuildRunner( companion object { /** Container label namespace; legacy used `org.hostsharing.werkator`. */ - const val werkator_LABEL = "org.hoennig.werkator" + const val WERKATOR_LABEL = "org.hoennig.werkator" fun gradleVolumeName(repoKey: String): String = "werkator-gradle-$repoKey" diff --git a/src/main/kotlin/de/hoennig/gittally/build/DockerImageInputs.kt b/src/main/kotlin/de/hoennig/werkator/build/DockerImageInputs.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/DockerImageInputs.kt rename to src/main/kotlin/de/hoennig/werkator/build/DockerImageInputs.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/DockerSocketLocator.kt b/src/main/kotlin/de/hoennig/werkator/build/DockerSocketLocator.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/DockerSocketLocator.kt rename to src/main/kotlin/de/hoennig/werkator/build/DockerSocketLocator.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/FileBuildResultRepository.kt b/src/main/kotlin/de/hoennig/werkator/build/FileBuildResultRepository.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/FileBuildResultRepository.kt rename to src/main/kotlin/de/hoennig/werkator/build/FileBuildResultRepository.kt diff --git a/src/main/kotlin/de/hoennig/gittally/build/RunningBuild.kt b/src/main/kotlin/de/hoennig/werkator/build/RunningBuild.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/build/RunningBuild.kt rename to src/main/kotlin/de/hoennig/werkator/build/RunningBuild.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/BranchNameResolution.kt b/src/main/kotlin/de/hoennig/werkator/commands/BranchNameResolution.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/BranchNameResolution.kt rename to src/main/kotlin/de/hoennig/werkator/commands/BranchNameResolution.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/BuildCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/BuildCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/ConfigPrintCommand.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/ConfigPrintCommand.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/ConsoleBuildRunner.kt b/src/main/kotlin/de/hoennig/werkator/commands/ConsoleBuildRunner.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/ConsoleBuildRunner.kt rename to src/main/kotlin/de/hoennig/werkator/commands/ConsoleBuildRunner.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/InitCommand.kt similarity index 97% rename from src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/InitCommand.kt index 8bd285a..449ba4d 100644 --- a/src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/InitCommand.kt @@ -13,7 +13,7 @@ import java.nio.file.Paths @Component @Command( name = "init", - description = ["Initialize werkator for the current repository"], + description = ["Initialize Werkator for the current repository"], mixinStandardHelpOptions = true, ) class InitCommand( @@ -130,10 +130,10 @@ class InitCommand( } val content = """ - # The werkator this file is written for. - # since: enforced — an older werkator refuses to read this file instead of + # The Werkator this file is written for. + # since: enforced — an older Werkator refuses to read this file instead of # silently ignoring the keys it does not know yet. - # below: your release marker for a coming major; werkator decides how strictly + # below: your release marker for a coming major; Werkator decides how strictly # to take it, and warns rather than blocks unless the format really broke. werkator: version: @@ -141,13 +141,13 @@ class InitCommand( # below: "2.0" 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: "" # HTTP port of the `server` subcommand port: 18080 # bind address of the `server` subcommand; loopback only, because the UI and the # API are unauthenticated — use 0.0.0.0 only without a reverse proxy in front - # (and with the managed nginx below, which reaches werkator from its container) + # (and with the managed nginx below, which reaches Werkator from its container) bindAddress: 127.0.0.1 # optional Impressum (legal disclosure) link in the web UI footer; empty hides the link impressumUrl: "" @@ -256,7 +256,7 @@ class InitCommand( ) { val jarPath = jarPathResolver() if (jarPath == null) { - println("Error: cannot determine the werkator jar path — run `init --systemd` via `java -jar /werkator.jar`") + println("Error: cannot determine the Werkator jar path — run `init --systemd` via `java -jar /werkator.jar`") return } val werkatorDir = root.resolve(".git/werkator") @@ -283,7 +283,7 @@ class InitCommand( } // the nightly Docker cleanup is host-global: every repository generates the same - // units, so with several werkator instances the symlinks simply coincide + // units, so with several Werkator instances the symlinks simply coincide val pruneServiceFile = werkatorDir.resolve(SystemdServiceFiles.PRUNE_SERVICE_NAME) val pruneTimerFile = werkatorDir.resolve(SystemdServiceFiles.PRUNE_TIMER_NAME) pruneServiceFile.toFile().writeText(SystemdServiceFiles.pruneServiceContent()) diff --git a/src/main/kotlin/de/hoennig/gittally/commands/RetryCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/RetryCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/ServerCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/ServerCommand.kt similarity index 96% rename from src/main/kotlin/de/hoennig/gittally/commands/ServerCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/ServerCommand.kt index 3c94653..d9a3112 100644 --- a/src/main/kotlin/de/hoennig/gittally/commands/ServerCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/ServerCommand.kt @@ -24,7 +24,7 @@ import java.util.concurrent.CountDownLatch @Component @Command( name = "server", - description = ["Start the werkator server"], + description = ["Start the Werkator server"], mixinStandardHelpOptions = true, ) class ServerCommand( @@ -43,7 +43,7 @@ class ServerCommand( "server.address=${config.server.bindAddress}", ).run() val port = context.environment.getProperty("local.server.port", config.server.port.toString()) - println("werkator server listening on http://${config.server.bindAddress}:$port/ — Ctrl-C to stop") + println("Werkator server listening on http://${config.server.bindAddress}:$port/ — Ctrl-C to stop") awaitShutdown(context) } diff --git a/src/main/kotlin/de/hoennig/gittally/commands/StatusCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/commands/StatusCommand.kt rename to src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt diff --git a/src/main/kotlin/de/hoennig/gittally/commands/SystemdServiceFiles.kt b/src/main/kotlin/de/hoennig/werkator/commands/SystemdServiceFiles.kt similarity index 90% rename from src/main/kotlin/de/hoennig/gittally/commands/SystemdServiceFiles.kt rename to src/main/kotlin/de/hoennig/werkator/commands/SystemdServiceFiles.kt index 4bd2bbc..4dbcd12 100644 --- a/src/main/kotlin/de/hoennig/gittally/commands/SystemdServiceFiles.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/SystemdServiceFiles.kt @@ -10,11 +10,11 @@ import java.nio.file.Path object SystemdServiceFiles { const val ENV_FILE_NAME = "werkator.env" - /** Host-global unit names of the nightly Docker cleanup — shared by all werkator repositories on the host. */ + /** Host-global unit names of the nightly Docker cleanup — shared by all Werkator repositories on the host. */ const val PRUNE_SERVICE_NAME = "werkator-docker-prune.service" const val PRUNE_TIMER_NAME = "werkator-docker-prune.timer" - /** Per-repository unit name, because one werkator instance serves exactly one repository. */ + /** Per-repository unit name, because one Werkator instance serves exactly one repository. */ fun unitName(repoRoot: Path): String = "werkator-${sanitize(repoRoot.fileName.toString())}.service" fun unitFileContent( @@ -25,7 +25,7 @@ object SystemdServiceFiles { ): String = """ [Unit] - Description=werkator CI for ${repoRoot.fileName} + Description=Werkator CI for ${repoRoot.fileName} Wants=network-online.target After=network-online.target docker.service @@ -49,7 +49,7 @@ object SystemdServiceFiles { fun pruneServiceContent(): String = """ [Unit] - Description=Clean up unused Docker containers and images (werkator) + Description=Clean up unused Docker containers and images (Werkator) [Service] Type=oneshot @@ -62,7 +62,7 @@ object SystemdServiceFiles { fun pruneTimerContent(): String = """ [Unit] - Description=Nightly Docker cleanup before the auto builds (werkator) + Description=Nightly Docker cleanup before the auto builds (Werkator) [Timer] OnCalendar=*-*-* 02:00:00 @@ -74,8 +74,8 @@ object SystemdServiceFiles { fun envFileContent(): String = """ - # EnvironmentFile for the werkator systemd service. - # werkator itself is configured via .werkator.yml and .git/werkator/.werkator.yml, + # EnvironmentFile for the Werkator systemd service. + # Werkator itself is configured via .werkator.yml and .git/werkator/.werkator.yml, # not via environment variables; this file only tunes the JVM process. #JAVA_OPTS=-Xmx256m """.trimIndent() + "\n" diff --git a/src/main/kotlin/de/hoennig/gittally/config/BuildDefinition.kt b/src/main/kotlin/de/hoennig/werkator/config/BuildDefinition.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/config/BuildDefinition.kt rename to src/main/kotlin/de/hoennig/werkator/config/BuildDefinition.kt diff --git a/src/main/kotlin/de/hoennig/werkator/config/ConfigFiles.kt b/src/main/kotlin/de/hoennig/werkator/config/ConfigFiles.kt new file mode 100644 index 0000000..13f7863 --- /dev/null +++ b/src/main/kotlin/de/hoennig/werkator/config/ConfigFiles.kt @@ -0,0 +1,47 @@ +package de.hoennig.werkator.config + +import java.nio.file.Files +import java.nio.file.Path + +/** + * The names a configuration file is looked up under, current name first and the name + * from before the rename to Werkator second — spelled exactly as it was. + * + * The fallback exists because a missing configuration is not an error: it leaves every + * setting at its default. An installation that updates without moving its files would + * therefore not fail, it would come up as a plausible-looking instance that has + * forgotten its credentials, its addresses, and what it builds. + */ +object ConfigFiles { + /** The committed configuration, at the repository root and in a build worktree. */ + const val COMMITTED = ".werkator.yml" + + /** The machine-specific configuration inside `.git`; secrets live here. */ + const val REPO_INSTALL = ".git/werkator/$COMMITTED" + + private const val LEGACY_COMMITTED = ".gittally.yml" + private const val LEGACY_REPO_INSTALL = ".git/gittally/$LEGACY_COMMITTED" + + /** Both names of the committed configuration, current first. */ + val committed = listOf(COMMITTED, LEGACY_COMMITTED) + + /** Both paths of the machine-specific configuration, current first. */ + val repoInstall = listOf(REPO_INSTALL, LEGACY_REPO_INSTALL) + + /** + * The first of [candidates] that exists under [dir], or the current name when none + * does — so a message about a file names the one to write, never the one that is + * history. + */ + fun firstExisting( + dir: Path, + candidates: List = committed, + ): String = candidates.firstOrNull { Files.isRegularFile(dir.resolve(it)) } ?: candidates.first() + + /** + * The committed configuration as [read] answers it for a name, current name first. + * Null when neither name is committed — used where the file is read out of git + * rather than off the filesystem. + */ + fun readCommitted(read: (String) -> String?): String? = committed.firstNotNullOfOrNull(read) +} diff --git a/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt b/src/main/kotlin/de/hoennig/werkator/config/ConfigLoader.kt similarity index 96% rename from src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt rename to src/main/kotlin/de/hoennig/werkator/config/ConfigLoader.kt index ac48b95..f208927 100644 --- a/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt +++ b/src/main/kotlin/de/hoennig/werkator/config/ConfigLoader.kt @@ -48,7 +48,7 @@ class ConfigLoader( fun loadForWorktree( workingDir: Path, worktreeDir: Path, - ): WerkatorConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(".werkator.yml").toFile())) + ): WerkatorConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(ConfigFiles.firstExisting(worktreeDir)).toFile())) /** * The primary/`.git` config with the committed `.werkator.yml` of one branch @@ -276,13 +276,16 @@ class ConfigLoader( } fun loadRaw(workingDir: Path = Paths.get(".")): Map { - val repoInstall = loadFile(workingDir.resolve(".git/werkator/.werkator.yml").toFile()) - val project = loadFile(workingDir.resolve(".werkator.yml").toFile()) + // each layer under its current name, or under the one it had before the rename + val repoInstallName = ConfigFiles.firstExisting(workingDir, ConfigFiles.repoInstall) + val projectName = ConfigFiles.firstExisting(workingDir) + val repoInstall = loadFile(workingDir.resolve(repoInstallName).toFile()) + val project = loadFile(workingDir.resolve(projectName).toFile()) // per file, so the message names the file to fix — the merged map has no provenance - checkVersion(project, ".werkator.yml", ROLLBACK_HINT) - checkVersion(repoInstall, ".git/werkator/.werkator.yml", ROLLBACK_HINT) - checkTriggerBlocks(project, ".werkator.yml", ROLLBACK_HINT) - checkTriggerBlocks(repoInstall, ".git/werkator/.werkator.yml", ROLLBACK_HINT) + checkVersion(project, projectName, ROLLBACK_HINT) + checkVersion(repoInstall, repoInstallName, ROLLBACK_HINT) + checkTriggerBlocks(project, projectName, ROLLBACK_HINT) + checkTriggerBlocks(repoInstall, repoInstallName, ROLLBACK_HINT) return deepMerge(project, repoInstall) } @@ -411,7 +414,7 @@ class ConfigLoader( private const val NO_TRIGGER_WARNING = "no-build-triggered" private const val ROLLBACK_HINT = - "Migrate the file, or roll back to the werkator version it was written for." + "Migrate the file, or roll back to the Werkator version it was written for." private const val BRANCH_HINT = "Migrate the file on this branch; the other branches keep building." diff --git a/src/main/kotlin/de/hoennig/gittally/config/ConfigVersion.kt b/src/main/kotlin/de/hoennig/werkator/config/ConfigVersion.kt similarity index 81% rename from src/main/kotlin/de/hoennig/gittally/config/ConfigVersion.kt rename to src/main/kotlin/de/hoennig/werkator/config/ConfigVersion.kt index d354176..6c3be27 100644 --- a/src/main/kotlin/de/hoennig/gittally/config/ConfigVersion.kt +++ b/src/main/kotlin/de/hoennig/werkator/config/ConfigVersion.kt @@ -1,33 +1,33 @@ package de.hoennig.werkator.config /** - * The werkator version a configuration file declares itself for, the `werkator.version` + * The Werkator version a configuration file declares itself for, the `werkator.version` * section: * * ```yaml * werkator: * version: - * since: "0.9.16" # always hard: an older werkator refuses this file - * below: "2.0" # werkator decides how hard, see ConfigVersions.verdict + * since: "0.9.16" # always hard: an older Werkator refuses this file + * below: "2.0" # Werkator decides how hard, see ConfigVersions.verdict * ``` * * There is deliberately no version of the file format itself (no `apiVersion`): no API is - * involved — werkator reads its own configuration — and only one configuration generation + * involved — Werkator reads its own configuration — and only one configuration generation * is ever supported. The declared version exists to make an incompatibility nameable, * never to run two parsers. */ data class VersionRequirement( - /** Oldest werkator that understands this file; empty means the file does not say. */ + /** Oldest Werkator that understands this file; empty means the file does not say. */ val since: String = "", - /** First werkator this file was not released for; empty means no ceiling. */ + /** First Werkator this file was not released for; empty means no ceiling. */ val below: String = "", ) -data class werkatorMeta( +data class WerkatorMeta( val version: VersionRequirement = VersionRequirement(), ) -/** What a [VersionRequirement] means for the werkator that reads the file. */ +/** What a [VersionRequirement] means for the Werkator that reads the file. */ sealed interface VersionVerdict { /** The running version is covered by the declaration. */ data object Compatible : VersionVerdict @@ -37,24 +37,24 @@ sealed interface VersionVerdict { val message: String, ) : VersionVerdict - /** Not usable: the file predates a change that werkator cannot bridge. */ + /** Not usable: the file predates a change that Werkator cannot bridge. */ data class Incompatible( val message: String, ) : VersionVerdict } -/** A configuration file this werkator must not read; carries the file's name in its message. */ +/** A configuration file this Werkator must not read; carries the file's name in its message. */ open class ConfigException( message: String, ) : RuntimeException(message) -/** The file declares a werkator that cannot read it, see [ConfigVersions]. */ +/** The file declares a Werkator that cannot read it, see [ConfigVersions]. */ class ConfigVersionException( message: String, ) : ConfigException(message) /** - * The file is written in a shape this werkator no longer reads. Refusing it is the point: + * The file is written in a shape this Werkator no longer reads. Refusing it is the point: * a key that moved and is silently ignored means a build that quietly stops happening. */ class ConfigFormatException( @@ -76,13 +76,13 @@ object ConfigVersions { /** * Decides what [requirement] means for [running]. * - * `since` is always hard — a file that needs a newer werkator cannot be honored, and + * `since` is always hard — a file that needs a newer Werkator cannot be honored, and * silently ignoring its unknown keys is exactly the failure mode this section exists * to prevent. * * `below` alone only warns: it is the team's release marker, and an unmaintained * marker must never stop a CI. Whether the running version really broke the file is - * werkator's own knowledge ([FORMAT_BROKE_IN]) — a file written before that change + * Werkator's own knowledge ([FORMAT_BROKE_IN]) — a file written before that change * and read after it is incompatible regardless of what it declares as its ceiling. */ fun verdict( @@ -95,13 +95,13 @@ object ConfigVersions { val since = parse(requirement.since) if (since != null && version < since) { return VersionVerdict.Incompatible( - "needs werkator ${requirement.since} or newer (werkator.version.since), this is $running", + "needs Werkator ${requirement.since} or newer (werkator.version.since), this is $running", ) } val broke = parse(brokeIn) if (since != null && broke != null && since < broke && version >= broke) { return VersionVerdict.Incompatible( - "is written for werkator ${requirement.since} (werkator.version.since), " + + "is written for Werkator ${requirement.since} (werkator.version.since), " + "but the configuration format changed incompatibly in $brokeIn" + brokeDescription.takeIf { it.isNotBlank() }?.let { ": $it" }.orEmpty(), ) @@ -109,7 +109,7 @@ object ConfigVersions { val below = parse(requirement.below) if (below != null && version >= below) { return VersionVerdict.Warn( - "was released for werkator below ${requirement.below} (werkator.version.below), this is $running", + "was released for Werkator below ${requirement.below} (werkator.version.below), this is $running", ) } return VersionVerdict.Compatible diff --git a/src/main/kotlin/de/hoennig/gittally/config/DurationParser.kt b/src/main/kotlin/de/hoennig/werkator/config/DurationParser.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/config/DurationParser.kt rename to src/main/kotlin/de/hoennig/werkator/config/DurationParser.kt diff --git a/src/main/kotlin/de/hoennig/gittally/config/WerkatorConfig.kt b/src/main/kotlin/de/hoennig/werkator/config/WerkatorConfig.kt similarity index 97% rename from src/main/kotlin/de/hoennig/gittally/config/WerkatorConfig.kt rename to src/main/kotlin/de/hoennig/werkator/config/WerkatorConfig.kt index 2eef6da..8555d01 100644 --- a/src/main/kotlin/de/hoennig/gittally/config/WerkatorConfig.kt +++ b/src/main/kotlin/de/hoennig/werkator/config/WerkatorConfig.kt @@ -3,8 +3,8 @@ package de.hoennig.werkator.config import com.fasterxml.jackson.annotation.JsonProperty data class WerkatorConfig( - /** What this file declares about the werkator that reads it; see [VersionRequirement]. */ - val werkator: werkatorMeta = werkatorMeta(), + /** What this file declares about the Werkator that reads it; see [VersionRequirement]. */ + val werkator: WerkatorMeta = WerkatorMeta(), val server: ServerConfig = ServerConfig(), val git: GitConfig = GitConfig(), val gitea: GiteaConfig = GiteaConfig(), @@ -61,7 +61,7 @@ data class ServerConfig( ) /** - * Opt-in managed nginx+certbot Docker container serving werkator over HTTPS, + * Opt-in managed nginx+certbot Docker container serving Werkator over HTTPS, * for hosts without a usable reverse proxy (ADR 0005). Off by default; the * reverse-proxy deployment from `docs/deployment.md` stays the recommended setup. */ diff --git a/src/main/kotlin/de/hoennig/gittally/git/GitAskPass.kt b/src/main/kotlin/de/hoennig/werkator/git/GitAskPass.kt similarity index 85% rename from src/main/kotlin/de/hoennig/gittally/git/GitAskPass.kt rename to src/main/kotlin/de/hoennig/werkator/git/GitAskPass.kt index d13d59a..83aad3b 100644 --- a/src/main/kotlin/de/hoennig/gittally/git/GitAskPass.kt +++ b/src/main/kotlin/de/hoennig/werkator/git/GitAskPass.kt @@ -15,10 +15,10 @@ object GitAskPass { #!/bin/sh case "${'$'}1" in *[Uu]sername*) - printf '%s\n' "${'$'}werkator_GIT_ACCOUNT" + printf '%s\n' "${'$'}WERKATOR_GIT_ACCOUNT" ;; *) - printf '%s\n' "${'$'}werkator_GIT_TOKEN" + printf '%s\n' "${'$'}WERKATOR_GIT_TOKEN" ;; esac """.trimIndent() + "\n" @@ -40,8 +40,8 @@ object GitAskPass { mapOf( "GIT_ASKPASS" to script.toAbsolutePath().toString(), "GIT_TERMINAL_PROMPT" to "0", - "werkator_GIT_ACCOUNT" to account, - "werkator_GIT_TOKEN" to token, + "WERKATOR_GIT_ACCOUNT" to account, + "WERKATOR_GIT_TOKEN" to token, ), ) } finally { diff --git a/src/main/kotlin/de/hoennig/gittally/git/GitCommandRunner.kt b/src/main/kotlin/de/hoennig/werkator/git/GitCommandRunner.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/git/GitCommandRunner.kt rename to src/main/kotlin/de/hoennig/werkator/git/GitCommandRunner.kt diff --git a/src/main/kotlin/de/hoennig/gittally/git/GitService.kt b/src/main/kotlin/de/hoennig/werkator/git/GitService.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/git/GitService.kt rename to src/main/kotlin/de/hoennig/werkator/git/GitService.kt diff --git a/src/main/kotlin/de/hoennig/gittally/gitea/GiteaClient.kt b/src/main/kotlin/de/hoennig/werkator/gitea/GiteaClient.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/gitea/GiteaClient.kt rename to src/main/kotlin/de/hoennig/werkator/gitea/GiteaClient.kt diff --git a/src/main/kotlin/de/hoennig/gittally/gitea/GiteaStateMapping.kt b/src/main/kotlin/de/hoennig/werkator/gitea/GiteaStateMapping.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/gitea/GiteaStateMapping.kt rename to src/main/kotlin/de/hoennig/werkator/gitea/GiteaStateMapping.kt diff --git a/src/main/kotlin/de/hoennig/gittally/metrics/MetricsConfiguration.kt b/src/main/kotlin/de/hoennig/werkator/metrics/MetricsConfiguration.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/metrics/MetricsConfiguration.kt rename to src/main/kotlin/de/hoennig/werkator/metrics/MetricsConfiguration.kt diff --git a/src/main/kotlin/de/hoennig/gittally/metrics/SystemMetrics.kt b/src/main/kotlin/de/hoennig/werkator/metrics/SystemMetrics.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/metrics/SystemMetrics.kt rename to src/main/kotlin/de/hoennig/werkator/metrics/SystemMetrics.kt diff --git a/src/main/kotlin/de/hoennig/gittally/metrics/SystemMetricsCollector.kt b/src/main/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollector.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/metrics/SystemMetricsCollector.kt rename to src/main/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollector.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ApiDtos.kt b/src/main/kotlin/de/hoennig/werkator/server/ApiDtos.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ApiDtos.kt rename to src/main/kotlin/de/hoennig/werkator/server/ApiDtos.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ArtifactFileController.kt b/src/main/kotlin/de/hoennig/werkator/server/ArtifactFileController.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ArtifactFileController.kt rename to src/main/kotlin/de/hoennig/werkator/server/ArtifactFileController.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/BranchListing.kt b/src/main/kotlin/de/hoennig/werkator/server/BranchListing.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/BranchListing.kt rename to src/main/kotlin/de/hoennig/werkator/server/BranchListing.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/BranchPermalinks.kt b/src/main/kotlin/de/hoennig/werkator/server/BranchPermalinks.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/BranchPermalinks.kt rename to src/main/kotlin/de/hoennig/werkator/server/BranchPermalinks.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt b/src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt rename to src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ControlTokenService.kt b/src/main/kotlin/de/hoennig/werkator/server/ControlTokenService.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ControlTokenService.kt rename to src/main/kotlin/de/hoennig/werkator/server/ControlTokenService.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/NginxConfigFiles.kt b/src/main/kotlin/de/hoennig/werkator/server/NginxConfigFiles.kt similarity index 98% rename from src/main/kotlin/de/hoennig/gittally/server/NginxConfigFiles.kt rename to src/main/kotlin/de/hoennig/werkator/server/NginxConfigFiles.kt index aec8664..8a08362 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/NginxConfigFiles.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/NginxConfigFiles.kt @@ -12,7 +12,7 @@ object NginxConfigFiles { * The `nginx.conf` content. Without [full] it is the init config for the * two-phase startup: HTTP only, serving the ACME webroot challenge and * redirecting everything else to HTTPS. With [full] an HTTPS server block - * with the Let's Encrypt certificate and the proxy to werkator is added. + * with the Let's Encrypt certificate and the proxy to Werkator is added. */ fun nginxConf( serverName: String, diff --git a/src/main/kotlin/de/hoennig/gittally/server/NginxProxyManager.kt b/src/main/kotlin/de/hoennig/werkator/server/NginxProxyManager.kt similarity index 96% rename from src/main/kotlin/de/hoennig/gittally/server/NginxProxyManager.kt rename to src/main/kotlin/de/hoennig/werkator/server/NginxProxyManager.kt index 815f1f2..8e4155a 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/NginxProxyManager.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/NginxProxyManager.kt @@ -1,7 +1,7 @@ package de.hoennig.werkator.server import de.hoennig.werkator.build.ArtifactKeys -import de.hoennig.werkator.build.DockerBuildRunner.Companion.werkator_LABEL +import de.hoennig.werkator.build.DockerBuildRunner.Companion.WERKATOR_LABEL import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.git.GitCommandRunner import org.slf4j.LoggerFactory @@ -11,7 +11,7 @@ import java.nio.file.Path import java.nio.file.Paths /** - * Manages the opt-in nginx+certbot Docker container that serves werkator over + * Manages the opt-in nginx+certbot Docker container that serves Werkator over * HTTPS on hosts without a reverse proxy (ADR 0005), ported from the legacy * `start_artifact_nginx` subsystem. Shells out to the `docker` CLI via the * generic [GitCommandRunner] process wrapper, like [de.hoennig.werkator.build.DockerBuildRunner]. @@ -197,7 +197,7 @@ class NginxProxyManager( /** * Legacy `cleanup_stale_artifact_nginx_containers`: remove the container by * name, all nginx-role containers of this repository by label, and any - * werkator container still occupying the configured ports. + * Werkator container still occupying the configured ports. */ private fun cleanupStaleContainers(settings: NginxSettings) { commandRunner.run(listOf("docker", "rm", "-f", settings.containerName), workingDir) @@ -208,11 +208,11 @@ class NginxProxyManager( "ps", "-aq", "--filter", - "label=$werkator_LABEL=true", + "label=$WERKATOR_LABEL=true", "--filter", - "label=$werkator_LABEL.repository=${settings.repoKey}", + "label=$WERKATOR_LABEL.repository=${settings.repoKey}", "--filter", - "label=$werkator_LABEL.role=nginx", + "label=$WERKATOR_LABEL.role=nginx", ), workingDir, ) @@ -220,11 +220,11 @@ class NginxProxyManager( commandRunner.run(listOf("docker", "rm", "-f") + labelled.lines(), workingDir) } for (container in listContainersUsingPorts(settings)) { - if (container.labels.contains("$werkator_LABEL=true") || + if (container.labels.contains("$WERKATOR_LABEL=true") || container.name.startsWith("werkator-") || container.name.startsWith("git-watch-origin-and-test-nginx-") ) { - log.info("removing stale werkator container using an nginx port: {}", container.name) + log.info("removing stale Werkator container using an nginx port: {}", container.name) commandRunner.run(listOf("docker", "rm", "-f", container.id), workingDir) } } @@ -303,11 +303,11 @@ class NginxProxyManager( "--volume", "${settings.nginxConf}:/etc/nginx/nginx.conf:ro", "--label", - "$werkator_LABEL=true", + "$WERKATOR_LABEL=true", "--label", - "$werkator_LABEL.repository=${settings.repoKey}", + "$WERKATOR_LABEL.repository=${settings.repoKey}", "--label", - "$werkator_LABEL.role=nginx", + "$WERKATOR_LABEL.role=nginx", "nginx", ) diff --git a/src/main/kotlin/de/hoennig/gittally/server/ServerConfiguration.kt b/src/main/kotlin/de/hoennig/werkator/server/ServerConfiguration.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ServerConfiguration.kt rename to src/main/kotlin/de/hoennig/werkator/server/ServerConfiguration.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ServerMetricsLifecycle.kt b/src/main/kotlin/de/hoennig/werkator/server/ServerMetricsLifecycle.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ServerMetricsLifecycle.kt rename to src/main/kotlin/de/hoennig/werkator/server/ServerMetricsLifecycle.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ServerNginxLifecycle.kt b/src/main/kotlin/de/hoennig/werkator/server/ServerNginxLifecycle.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ServerNginxLifecycle.kt rename to src/main/kotlin/de/hoennig/werkator/server/ServerNginxLifecycle.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/ServerWatcherLifecycle.kt b/src/main/kotlin/de/hoennig/werkator/server/ServerWatcherLifecycle.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/ServerWatcherLifecycle.kt rename to src/main/kotlin/de/hoennig/werkator/server/ServerWatcherLifecycle.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/StatusApiController.kt b/src/main/kotlin/de/hoennig/werkator/server/StatusApiController.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/StatusApiController.kt rename to src/main/kotlin/de/hoennig/werkator/server/StatusApiController.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/SystemApiController.kt b/src/main/kotlin/de/hoennig/werkator/server/SystemApiController.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/SystemApiController.kt rename to src/main/kotlin/de/hoennig/werkator/server/SystemApiController.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt similarity index 99% rename from src/main/kotlin/de/hoennig/gittally/server/UiController.kt rename to src/main/kotlin/de/hoennig/werkator/server/UiController.kt index e4b5c51..d198c9c 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt @@ -5,10 +5,10 @@ import de.hoennig.werkator.build.BuildExecutor import de.hoennig.werkator.build.BuildResult import de.hoennig.werkator.build.BuildResultRepository import de.hoennig.werkator.build.BuildStatus +import de.hoennig.werkator.config.ConfigFiles import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.git.GitService import de.hoennig.werkator.metrics.SystemMetricsCollector -import de.hoennig.werkator.watcher.Watcher import jakarta.servlet.http.HttpServletRequest import org.springframework.beans.factory.ObjectProvider import org.springframework.boot.info.BuildProperties @@ -235,7 +235,7 @@ class UiController( try { configLoader.loadWithBranchLayer( workingDir, - gitService.showFileAtCommit(result.commit, Watcher.CONFIG_FILE, workingDir), + ConfigFiles.readCommitted { gitService.showFileAtCommit(result.commit, it, workingDir) }, ) } catch (_: Exception) { configLoader.load(workingDir) diff --git a/src/main/kotlin/de/hoennig/gittally/server/UiViews.kt b/src/main/kotlin/de/hoennig/werkator/server/UiViews.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/UiViews.kt rename to src/main/kotlin/de/hoennig/werkator/server/UiViews.kt diff --git a/src/main/kotlin/de/hoennig/gittally/server/WatcherApiController.kt b/src/main/kotlin/de/hoennig/werkator/server/WatcherApiController.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/server/WatcherApiController.kt rename to src/main/kotlin/de/hoennig/werkator/server/WatcherApiController.kt diff --git a/src/main/kotlin/de/hoennig/gittally/watcher/AutoBuildState.kt b/src/main/kotlin/de/hoennig/werkator/watcher/AutoBuildState.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/watcher/AutoBuildState.kt rename to src/main/kotlin/de/hoennig/werkator/watcher/AutoBuildState.kt diff --git a/src/main/kotlin/de/hoennig/gittally/watcher/Watcher.kt b/src/main/kotlin/de/hoennig/werkator/watcher/Watcher.kt similarity index 98% rename from src/main/kotlin/de/hoennig/gittally/watcher/Watcher.kt rename to src/main/kotlin/de/hoennig/werkator/watcher/Watcher.kt index 6427356..cada592 100644 --- a/src/main/kotlin/de/hoennig/gittally/watcher/Watcher.kt +++ b/src/main/kotlin/de/hoennig/werkator/watcher/Watcher.kt @@ -7,6 +7,7 @@ import de.hoennig.werkator.build.BuildResultRepository import de.hoennig.werkator.build.BuildStatus import de.hoennig.werkator.build.GitWorktreeWorkspaces import de.hoennig.werkator.config.BuildDefinition +import de.hoennig.werkator.config.ConfigFiles import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.config.DurationParser import de.hoennig.werkator.config.WerkatorConfig @@ -261,8 +262,10 @@ class Watcher( val definitions = try { configLoader - .loadWithBranchLayer(workingDir, gitService.showFileAtCommit(commit, CONFIG_FILE, workingDir)) - .effectiveBuildDefinitions() + .loadWithBranchLayer( + workingDir, + ConfigFiles.readCommitted { gitService.showFileAtCommit(commit, it, workingDir) }, + ).effectiveBuildDefinitions() } catch (e: Exception) { log.warn( "ignoring the committed {} of branch {} at {}: {}", @@ -466,6 +469,6 @@ class Watcher( const val AUTO_BUILDS_FILE = ".git/werkator/auto-builds.json" /** The committed config read per branch for its build definitions. */ - const val CONFIG_FILE = ".werkator.yml" + const val CONFIG_FILE = ConfigFiles.COMMITTED } } diff --git a/src/main/kotlin/de/hoennig/gittally/watcher/WatcherConfiguration.kt b/src/main/kotlin/de/hoennig/werkator/watcher/WatcherConfiguration.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/watcher/WatcherConfiguration.kt rename to src/main/kotlin/de/hoennig/werkator/watcher/WatcherConfiguration.kt diff --git a/src/main/kotlin/de/hoennig/gittally/watcher/WatcherState.kt b/src/main/kotlin/de/hoennig/werkator/watcher/WatcherState.kt similarity index 100% rename from src/main/kotlin/de/hoennig/gittally/watcher/WatcherState.kt rename to src/main/kotlin/de/hoennig/werkator/watcher/WatcherState.kt diff --git a/src/main/resources/static/favicon.svg b/src/main/resources/static/favicon.svg index 8eac398..ed040f1 100644 --- a/src/main/resources/static/favicon.svg +++ b/src/main/resources/static/favicon.svg @@ -1,4 +1,4 @@ - + diff --git a/src/main/resources/static/gittally.css b/src/main/resources/static/werkator.css similarity index 99% rename from src/main/resources/static/gittally.css rename to src/main/resources/static/werkator.css index 0ce508c..450299a 100644 --- a/src/main/resources/static/gittally.css +++ b/src/main/resources/static/werkator.css @@ -1,4 +1,4 @@ -/* werkator web UI — loosely ported from the legacy generated pages. */ +/* Werkator web UI — loosely ported from the legacy generated pages. */ :root { color-scheme: light dark; diff --git a/src/main/resources/static/gittally.js b/src/main/resources/static/werkator.js similarity index 99% rename from src/main/resources/static/gittally.js rename to src/main/resources/static/werkator.js index d7a9412..0e70b82 100644 --- a/src/main/resources/static/gittally.js +++ b/src/main/resources/static/werkator.js @@ -1,4 +1,4 @@ -// werkator web UI — polls the JSON API and re-renders table bodies from data. +// Werkator web UI — polls the JSON API and re-renders table bodies from data. // Every fetch has a timeout and failures render an explicit error badge, so the // UI can never get stuck on a loading animation (the legacy defect). "use strict"; @@ -131,7 +131,7 @@ function forgetControlToken() { function askForControlToken() { const answer = window.prompt( - "Control token — the content of .git/werkator/control-token on the werkator host:", + "Control token — the content of .git/werkator/control-token on the Werkator host:", "", ); return answer ? answer.trim() : ""; diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html index 8cdfd95..d1b3e93 100644 --- a/src/main/resources/templates/fragments.html +++ b/src/main/resources/templates/fragments.html @@ -3,7 +3,7 @@ - werkator + Werkator @@ -40,7 +40,7 @@