Retarget the links in the historic PR-docs

The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-31 07:34:39 +02:00
co-authored by Claude Opus 5
parent 56fd39f1ad
commit e21c94d0e7
7 changed files with 53 additions and 53 deletions
@@ -36,8 +36,8 @@ So that stale logs and reports do not stay on disk indefinitely.
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
- [WatcherTest](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.02: Count and age combine as independent caps #### Scenario#000.02: Count and age combine as independent caps
@@ -50,7 +50,7 @@ So that one config can bound disk usage by count and staleness by age at the sam
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.03: A branch's newest build is never age-pruned #### Scenario#000.03: A branch's newest build is never age-pruned
@@ -62,7 +62,7 @@ So that a dormant branch keeps its last build status visible, matching the legac
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.04: keepLatestGreen shields the latest green build from the age limit #### Scenario#000.04: keepLatestGreen shields the latest green build from the age limit
@@ -75,7 +75,7 @@ So that the permanent `/branches/<branch-key>/…` artifact links stay valid whi
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.05: The default keeps existing behavior unchanged #### Scenario#000.05: The default keeps existing behavior unchanged
@@ -87,7 +87,7 @@ So that existing installations are unaffected by the new key.
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) (pre-existing count-only prune tests) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt) (pre-existing count-only prune tests)
## The Solution ## The Solution
@@ -39,7 +39,7 @@ So that pull-request branches get their commit status verified as before.
##### Verified by ##### Verified by
- [WatcherTest: "a branch requiring a pull request is only built when its head matches a pull-request head"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "a branch requiring a pull request is only built when its head matches a pull-request head"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.02: A gated branch without a pull request is not built! #### Scenario#000.02: A gated branch without a pull request is not built!
@@ -54,7 +54,7 @@ So that work-in-progress branches do not consume build capacity.
##### Verified by ##### Verified by
- [WatcherTest: "a branch requiring a pull request is only built when its head matches a pull-request head"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "a branch requiring a pull request is only built when its head matches a pull-request head"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.03: Pull-request detection works without an API token! #### Scenario#000.03: Pull-request detection works without an API token!
@@ -66,7 +66,7 @@ So that GitTally needs no Gitea credentials for this feature.
##### Verified by ##### Verified by
- [GitServiceTest: "pullRequestHeads returns the head commits of the remote's pull-request refs"](../../src/test/kotlin/de/hoennig/gittally/git/GitServiceTest.kt) - [GitServiceTest: "pullRequestHeads returns the head commits of the remote's pull-request refs"](../../src/test/kotlin/de/hoennig/werkator/git/GitServiceTest.kt)
#### Scenario#000.04: Ungated setups make no extra remote calls! #### Scenario#000.04: Ungated setups make no extra remote calls!
@@ -78,7 +78,7 @@ So that existing installations see no new network traffic.
##### Verified by ##### Verified by
- [WatcherTest: "pull-request refs are not queried when no due branch requires a pull request"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "pull-request refs are not queried when no due branch requires a pull request"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.05: A branch entry overrides the default! #### Scenario#000.05: A branch entry overrides the default!
@@ -91,7 +91,7 @@ So that permanent branches like `main` keep building after merges, whose merge c
##### Verified by ##### Verified by
- [WatcherTest: "a branch entry overrides requirePullRequest from the default entry"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "a branch entry overrides requirePullRequest from the default entry"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.06: Auto builds respect the gate! #### Scenario#000.06: Auto builds respect the gate!
@@ -105,7 +105,7 @@ So that scheduled rebuilds follow the same policy as push-triggered builds.
##### Verified by ##### Verified by
- [WatcherTest: "an auto build requiring a pull request is skipped and its slot stays untriggered"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "an auto build requiring a pull request is skipped and its slot stays untriggered"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.07: The gate can be disabled globally for plain git origins! #### Scenario#000.07: The gate can be disabled globally for plain git origins!
@@ -119,7 +119,7 @@ So that the same committed configuration works on environments whose origin is p
##### Verified by ##### Verified by
- [WatcherTest: "a disabled pull-request gate builds gated branches on plain-git origins without querying pull-request refs"](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest: "a disabled pull-request gate builds gated branches on plain-git origins without querying pull-request refs"](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
## The Solution ## The Solution
@@ -34,8 +34,8 @@ So that links in READMEs, wikis, and bookmarks stay valid across new builds.
##### Verified by ##### Verified by
- [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/ArtifactFileControllerTest.kt) - [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerTest.kt)
- [BranchPermalinksTest](../../src/test/kotlin/de/hoennig/gittally/server/BranchPermalinksTest.kt) - [BranchPermalinksTest](../../src/test/kotlin/de/hoennig/werkator/server/BranchPermalinksTest.kt)
#### Scenario#000.02: Directory URLs serve their index page like a static web server #### Scenario#000.02: Directory URLs serve their index page like a static web server
@@ -49,7 +49,7 @@ So that legacy-style report links such as `/branches/main/reports/build/doc/` wo
##### Verified by ##### Verified by
- [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/ArtifactFileControllerTest.kt) - [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerTest.kt)
#### Scenario#000.03: The bare permanent URL renders a permanent artifact index #### Scenario#000.03: The bare permanent URL renders a permanent artifact index
@@ -63,8 +63,8 @@ So that users can browse the latest green artifacts from one stable bookmark.
##### Verified by ##### Verified by
- [UiControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt) - [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
- [PermanentBranchRoutesTest](../../src/test/kotlin/de/hoennig/gittally/server/PermanentBranchRoutesTest.kt) (the `/branches`, `/branches/<key>`, and `/branches/<key>/<path>` routes coexist) - [PermanentBranchRoutesTest](../../src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt) (the `/branches`, `/branches/<key>`, and `/branches/<key>/<path>` routes coexist)
#### Scenario#000.04: Green-only and unambiguous resolution #### Scenario#000.04: Green-only and unambiguous resolution
@@ -80,7 +80,7 @@ So that a permanent link never points at broken artifacts or the wrong branch.
##### Verified by ##### Verified by
- [BranchPermalinksTest](../../src/test/kotlin/de/hoennig/gittally/server/BranchPermalinksTest.kt) - [BranchPermalinksTest](../../src/test/kotlin/de/hoennig/werkator/server/BranchPermalinksTest.kt)
#### Scenario#000.05: The latest green build survives pruning #### Scenario#000.05: The latest green build survives pruning
@@ -94,8 +94,8 @@ So that a permanent link stays valid while newer builds fail, as long as the bra
##### Verified by ##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) - [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
- [WatcherTest](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) - [WatcherTest](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
#### Scenario#000.06: The branches view links the permanent URL #### Scenario#000.06: The branches view links the permanent URL
@@ -108,8 +108,8 @@ So that users can discover the permanent link without constructing it by hand.
##### Verified by ##### Verified by
- [BranchListingTest](../../src/test/kotlin/de/hoennig/gittally/server/BranchListingTest.kt) - [BranchListingTest](../../src/test/kotlin/de/hoennig/werkator/server/BranchListingTest.kt)
- [UiControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt) - [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
## The Solution ## The Solution
@@ -42,8 +42,8 @@ Each item is a TODO with the background that justifies it.
#### TODO 1 — Restrict the Gitea-token config file to the owner at creation #### TODO 1 — Restrict the Gitea-token config file to the owner at creation
- [x] In [`InitCommand.kt:86-106`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt), create `.git/gittally/.gittally.yml` and its parent `.git/gittally/` with `0600`/`0700`, atomically at creation (as [`GitAskPass`](../../src/main/kotlin/de/hoennig/gittally/git/GitAskPass.kt) does), not via plain `writeText` at the umask default. - [x] In [`InitCommand.kt:86-106`](../../src/main/kotlin/de/hoennig/werkator/commands/InitCommand.kt), create `.git/gittally/.gittally.yml` and its parent `.git/gittally/` with `0600`/`0700`, atomically at creation (as [`GitAskPass`](../../src/main/kotlin/de/hoennig/werkator/git/GitAskPass.kt) does), not via plain `writeText` at the umask default.
**Done:** both `init` paths now go through [`SecretFiles`](../../src/main/kotlin/de/hoennig/gittally/SecretFiles.kt), which sets the mode as a file attribute at creation. **Done:** both `init` paths now go through [`SecretFiles`](../../src/main/kotlin/de/hoennig/werkator/SecretFiles.kt), which sets the mode as a file attribute at creation.
**Background.** **Background.**
`init` creates the file it labels "secrets" — where the operator pastes the Gitea API token — with `writeText` and no permission restriction, so it inherits the umask (typically `0644`, world-readable). `init` creates the file it labels "secrets" — where the operator pastes the Gitea API token — with `writeText` and no permission restriction, so it inherits the umask (typically `0644`, world-readable).
@@ -52,7 +52,7 @@ On a shared host (Hostsharing is a multi-tenant deployment target, per ADR 0005)
#### TODO 2 — Stop handing the control token to every unauthenticated reader #### TODO 2 — Stop handing the control token to every unauthenticated reader
- [x] Reconsider the token distribution in [`UiController.kt:186`](../../src/main/kotlin/de/hoennig/gittally/server/UiController.kt) and [`fragments.html:9`](../../src/main/resources/templates/fragments.html): do not embed the live token in public HTML, or gate the pages behind the same check as the mutations. - [x] Reconsider the token distribution in [`UiController.kt:186`](../../src/main/kotlin/de/hoennig/werkator/server/UiController.kt) and [`fragments.html:9`](../../src/main/resources/templates/fragments.html): do not embed the live token in public HTML, or gate the pages behind the same check as the mutations.
**Done in v0.9.10, without gating the pages.** Public read access is a requirement, not an oversight: build states, logs and artifacts must stay linkable without a login. **Done in v0.9.10, without gating the pages.** Public read access is a requirement, not an oversight: build states, logs and artifacts must stay linkable without a login.
So the `<meta>` tag is gone and `gittally.js` keeps the token in `localStorage`, asking for it once per browser (the operator reads it from `.git/gittally/control-token`, which needs shell access to the host). So the `<meta>` tag is gone and `gittally.js` keeps the token in `localStorage`, asking for it once per browser (the operator reads it from `.git/gittally/control-token`, which needs shell access to the host).
The token is a real secret again, and as a request header it stays inherently CSRF-safe — a foreign origin cannot set it without a CORS grant. The token is a real secret again, and as a request header it stays inherently CSRF-safe — a foreign origin cannot set it without a CORS grant.
@@ -61,7 +61,7 @@ On a shared host (Hostsharing is a multi-tenant deployment target, per ADR 0005)
Superseded: read access no longer implies write access. `docs/deployment.md` gained a "Control Token" section describing the split. Superseded: read access no longer implies write access. `docs/deployment.md` gained a "Control Token" section describing the split.
**Background.** **Background.**
Every server-rendered page embeds the live control token in `<meta name="gittally-control-token">` so [`gittally.js`](../../src/main/resources/static/gittally.js) can read it, but no GET is authenticated. Every server-rendered page embeds the live control token in `<meta name="gittally-control-token">` so [`gittally.js`](../../src/main/resources/static/werkator.js) can read it, but no GET is authenticated.
So `curl -s http://host:18080/ | grep gittally-control-token` yields the token, which unlocks restart/cancel/delete. So `curl -s http://host:18080/ | grep gittally-control-token` yields the token, which unlocks restart/cancel/delete.
Read access therefore equals write access, and the token provides no real second trust tier. Read access therefore equals write access, and the token provides no real second trust tier.
Blast radius is limited to build-lifecycle operations (a DoS/integrity concern, not secret disclosure or RCE), which is why this is a design flaw rather than Critical — but the token gives a false sense of protection. Blast radius is limited to build-lifecycle operations (a DoS/integrity concern, not secret disclosure or RCE), which is why this is a design flaw rather than Critical — but the token gives a false sense of protection.
@@ -70,7 +70,7 @@ Blast radius is limited to build-lifecycle operations (a DoS/integrity concern,
#### TODO 3 — Accept the control token via header only #### TODO 3 — Accept the control token via header only
- [x] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses). - [x] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses).
**Background.** **Background.**
Tokens in URLs are routinely written to access logs, reverse-proxy logs, browser history, and the `Referer` header on outbound navigation. Tokens in URLs are routinely written to access logs, reverse-proxy logs, browser history, and the `Referer` header on outbound navigation.
@@ -79,7 +79,7 @@ The query-param path exists only for legacy convenience.
#### TODO 4 — Redact the Gitea token in `config:print` #### TODO 4 — Redact the Gitea token in `config:print`
- [x] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag. - [x] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/werkator/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag.
Masked as `***` on both the `--full` and the raw path, with a leading YAML comment naming the flag, so the output stays parseable when piped. Masked as `***` on both the `--full` and the raw path, with a leading YAML comment naming the flag, so the output stays parseable when piped.
- [x] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token. - [x] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token.
@@ -89,7 +89,7 @@ There is no redaction and no masked default.
#### TODO 5 — Reduce information disclosure on the unauthenticated read API #### TODO 5 — Reduce information disclosure on the unauthenticated read API
- [x] Decide whether build-log streaming ([`BuildsApiController`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt) `/api/builds/current/{key}/log`), [`SystemApiController`](../../src/main/kotlin/de/hoennig/gittally/server/SystemApiController.kt), and [`WatcherApiController`](../../src/main/kotlin/de/hoennig/gittally/server/WatcherApiController.kt) should stay fully public, or be gated / scrubbed. - [x] Decide whether build-log streaming ([`BuildsApiController`](../../src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt) `/api/builds/current/{key}/log`), [`SystemApiController`](../../src/main/kotlin/de/hoennig/werkator/server/SystemApiController.kt), and [`WatcherApiController`](../../src/main/kotlin/de/hoennig/werkator/server/WatcherApiController.kt) should stay fully public, or be gated / scrubbed.
**Decided: they stay fully public, no scrubbing.** The watched projects (GitTally itself and hs.hsadmin.ng) are open source, the repositories hold no secrets, and the builds run tests against test data — credentials appearing in a log are fixtures, not real ones. Builds neither deploy nor sign; the only planned artifact is a jar. **Decided: they stay fully public, no scrubbing.** The watched projects (GitTally itself and hs.hsadmin.ng) are open source, the repositories hold no secrets, and the builds run tests against test data — credentials appearing in a log are fixtures, not real ones. Builds neither deploy nor sign; the only planned artifact is a jar.
Public logs are also the point of the tool: a red build must be diagnosable from the link in the Gitea status without a login. Public logs are also the point of the tool: a red build must be diagnosable from the link in the Gitea status without a login.
This is a property of the watched project, not of GitTally: an installation whose builds touch real credentials must keep its instance off the public internet (reverse proxy or `bindAddress: 127.0.0.1`), because GitTally offers no per-endpoint gating. This is a property of the watched project, not of GitTally: an installation whose builds touch real credentials must keep its instance off the public internet (reverse proxy or `bindAddress: 127.0.0.1`), because GitTally offers no per-endpoint gating.
@@ -104,14 +104,14 @@ Raw build output may contain secrets echoed by build scripts; `/api/system` expo
A branch is built with its own build settings: `.gittally.yml` from the **build worktree** (the commit being built) overrides the `.git`/primary config — except for a pinned set that a branch must never control. **Implemented in this PR.** A branch is built with its own build settings: `.gittally.yml` from the **build worktree** (the commit being built) overrides the `.git`/primary config — except for a pinned set that a branch must never control. **Implemented in this PR.**
- [x] Worktree config layer for builds via [`ConfigLoader.loadForWorktree`](../../src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt), wired into the two build-time config consumers ([`BuildExecutor.branchConfig`](../../src/main/kotlin/de/hoennig/gittally/build/BuildExecutor.kt), [`FileArtifactStore.branchConfig`](../../src/main/kotlin/de/hoennig/gittally/artifacts/FileArtifactStore.kt)) — both already hold the prepared worktree path. Precedence is worktree > `.git` > project. - [x] Worktree config layer for builds via [`ConfigLoader.loadForWorktree`](../../src/main/kotlin/de/hoennig/werkator/config/ConfigLoader.kt), wired into the two build-time config consumers ([`BuildExecutor.branchConfig`](../../src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt), [`FileArtifactStore.branchConfig`](../../src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt)) — both already hold the prepared worktree path. Precedence is worktree > `.git` > project.
- [x] **Pinned to `.git`/primary — stripped from the worktree layer:** `git`, `gitea`, `server` (secrets + server-side), and the sandbox policy `docker.enabled`/`docker.network`. A branch cannot disable its container or change its network mode. - [x] **Pinned to `.git`/primary — stripped from the worktree layer:** `git`, `gitea`, `server` (secrets + server-side), and the sandbox policy `docker.enabled`/`docker.network`. A branch cannot disable its container or change its network mode.
- [x] Worktree-overridable: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `autoBuild`, and `docker.image`/`dockerfile`/`context`/`env`. - [x] Worktree-overridable: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, `autoBuild`, and `docker.image`/`dockerfile`/`context`/`env`.
- [x] Pinned set enforced in code (`ConfigLoader.stripPinned`), documented in `docs/configuration.md`, and asserted as an invariant in `AGENTS.md`. - [x] Pinned set enforced in code (`ConfigLoader.stripPinned`), documented in `docs/configuration.md`, and asserted as an invariant in `AGENTS.md`.
- [ ] **Deferred:** `autoBuild` scheduling and `requirePullRequest` are still read from the primary config, not the worktree — the watcher evaluates them *before* a build (and thus a worktree) exists. Sourcing them per-branch would need the watcher to read the branch's committed config directly (e.g. via `git show <branch>:.gittally.yml`); out of scope here. - [ ] **Deferred:** `autoBuild` scheduling and `requirePullRequest` are still read from the primary config, not the worktree — the watcher evaluates them *before* a build (and thus a worktree) exists. Sourcing them per-branch would need the watcher to read the branch's committed config directly (e.g. via `git show <branch>:.gittally.yml`); out of scope here.
**Background.** **Background.**
The build command is executed via `bash -c "$3"` inside the build container ([`DockerBuildRunner.kt:285`](../../src/main/kotlin/de/hoennig/gittally/build/DockerBuildRunner.kt)). The build command is executed via `bash -c "$3"` inside the build container ([`DockerBuildRunner.kt:285`](../../src/main/kotlin/de/hoennig/werkator/build/DockerBuildRunner.kt)).
Letting a branch define its own `buildCommand` is not a new risk — a CI already runs arbitrary code from that commit; the container is the sandbox. Letting a branch define its own `buildCommand` is not a new risk — a CI already runs arbitrary code from that commit; the container is the sandbox.
The real escalation is a branch turning the sandbox **off**: if the worktree could set `docker.enabled: false` (or host `docker.network`), the build would run natively on the host — which is why those two keys are pinned. The real escalation is a branch turning the sandbox **off**: if the worktree could set `docker.enabled: false` (or host `docker.network`), the build would run natively on the host — which is why those two keys are pinned.
Secrets are also safe from the build process (the Gitea token is used only by the server/watcher and is never placed in the build environment — `runCommand` passes only `mapOf("branch" to ...)`), and the whole `git`/`gitea`/`server` sections are stripped from the worktree layer as defense in depth. Secrets are also safe from the build process (the Gitea token is used only by the server/watcher and is never placed in the build environment — `runCommand` passes only `mapOf("branch" to ...)`), and the whole `git`/`gitea`/`server` sections are stripped from the worktree layer as defense in depth.
@@ -121,7 +121,7 @@ Before this PR all build config was loaded from the primary checkout via `config
#### TODO 7 — Default `bindAddress` to `127.0.0.1` #### TODO 7 — Default `bindAddress` to `127.0.0.1`
- [x] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/gittally/config/GitTallyConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces. - [x] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/werkator/config/WerkatorConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/werkator/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces.
Shipped as v0.9.9 with the migration note in the release notes, `docs/configuration.md` ("Notes on `server.bindAddress`") and `docs/deployment.md`: existing configs keep their explicit value, and the managed nginx now needs `0.0.0.0` set deliberately. Shipped as v0.9.9 with the migration note in the release notes, `docs/configuration.md` ("Notes on `server.bindAddress`") and `docs/deployment.md`: existing configs keep their explicit value, and the managed nginx now needs `0.0.0.0` set deliberately.
**Background.** **Background.**
@@ -130,7 +130,7 @@ The deployment doc already recommends `127.0.0.1`; the default and template shou
#### TODO 8 — Compare fixed-length hashes in the token check #### TODO 8 — Compare fixed-length hashes in the token check
- [x] In [`ControlTokenService.kt:35-37`](../../src/main/kotlin/de/hoennig/gittally/server/ControlTokenService.kt), compare `SHA-256(submitted)` against `SHA-256(secret)` with `MessageDigest.isEqual`, so the comparison is always over equal-length buffers. - [x] In [`ControlTokenService.kt:35-37`](../../src/main/kotlin/de/hoennig/werkator/server/ControlTokenService.kt), compare `SHA-256(submitted)` against `SHA-256(secret)` with `MessageDigest.isEqual`, so the comparison is always over equal-length buffers.
**Background.** **Background.**
`MessageDigest.isEqual` returns early on a length mismatch, leaking the token length via timing. `MessageDigest.isEqual` returns early on a length mismatch, leaking the token length via timing.
@@ -138,7 +138,7 @@ Largely theoretical given the 192-bit CSPRNG token, but a cheap deviation from c
#### TODO 9 — Add `--` before positional refnames in git calls #### TODO 9 — Add `--` before positional refnames in git calls
- [x] Insert `--` before the branch argument in `checkout`, `fetchBranch`, and `resetHardToOrigin` in [`GitService.kt:145,40,155`](../../src/main/kotlin/de/hoennig/gittally/git/GitService.kt) (e.g. `git switch -- <branch>`). - [x] Insert `--` before the branch argument in `checkout`, `fetchBranch`, and `resetHardToOrigin` in [`GitService.kt:145,40,155`](../../src/main/kotlin/de/hoennig/werkator/git/GitService.kt) (e.g. `git switch -- <branch>`).
**Done for `checkout` and `fetchBranch`.** `resetHardToOrigin` keeps its plain form: `git reset --hard -- <commit>` is rejected (`fatal: Cannot do hard reset with paths`), and its argument is already prefixed with `origin/`, so it can never start with `-`. **Done for `checkout` and `fetchBranch`.** `resetHardToOrigin` keeps its plain form: `git reset --hard -- <commit>` is rejected (`fatal: Cannot do hard reset with paths`), and its argument is already prefixed with `origin/`, so it can never start with `-`.
**Background.** **Background.**
@@ -147,8 +147,8 @@ These three methods currently have no production callers and the actively-used p
#### TODO 10 — Create secret files restricted atomically #### TODO 10 — Create secret files restricted atomically
- [x] Set the mode at creation for the control-token file ([`ControlTokenService.kt:29-30`](../../src/main/kotlin/de/hoennig/gittally/server/ControlTokenService.kt)) and the setup-script YAML (`tools/setup-gittally-instance:253`), instead of `chmod 0600` after the write. - [x] Set the mode at creation for the control-token file ([`ControlTokenService.kt:29-30`](../../src/main/kotlin/de/hoennig/werkator/server/ControlTokenService.kt)) and the setup-script YAML (`tools/setup-gittally-instance:253`), instead of `chmod 0600` after the write.
**Done:** the control-token file via [`SecretFiles`](../../src/main/kotlin/de/hoennig/gittally/SecretFiles.kt), the setup script by writing the YAML in a `umask 077` subshell instead of `chmod`-ing afterwards. **Done:** the control-token file via [`SecretFiles`](../../src/main/kotlin/de/hoennig/werkator/SecretFiles.kt), the setup script by writing the YAML in a `umask 077` subshell instead of `chmod`-ing afterwards.
**Background.** **Background.**
Both currently write the file at the umask default and tighten it afterward, leaving a brief window where the secret exists world-readable. Both currently write the file at the umask default and tighten it afterward, leaving a brief window where the secret exists world-readable.
@@ -175,11 +175,11 @@ A small TOCTOU gap; `GitAskPass`'s atomic-at-creation approach is the pattern to
Recorded so future changes do not silently regress these. Recorded so future changes do not silently regress these.
- **Path traversal in artifact serving** — [`ArtifactFileController`](../../src/main/kotlin/de/hoennig/gittally/server/ArtifactFileController.kt) normalizes then enforces `startsWith(artifactDir)` and `isRegularFile(..., NOFOLLOW_LINKS)` (rejects symlink escape); [`FileArtifactStore`](../../src/main/kotlin/de/hoennig/gittally/artifacts/FileArtifactStore.kt) whitelists the key to `[A-Za-z0-9._-]+` and requires `dir.parent == branchesDir`. - **Path traversal in artifact serving** — [`ArtifactFileController`](../../src/main/kotlin/de/hoennig/werkator/server/ArtifactFileController.kt) normalizes then enforces `startsWith(artifactDir)` and `isRegularFile(..., NOFOLLOW_LINKS)` (rejects symlink escape); [`FileArtifactStore`](../../src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt) whitelists the key to `[A-Za-z0-9._-]+` and requires `dir.parent == branchesDir`.
- **Command injection** — every git/docker/certbot/nginx call uses `ProcessBuilder(List<String>)` ([`GitCommandRunner.kt`](../../src/main/kotlin/de/hoennig/gittally/git/GitCommandRunner.kt)); no `Runtime.exec(String)`, no shell-string interpolation; the only `sh -c`/`bash -c` uses pass data as positional args. - **Command injection** — every git/docker/certbot/nginx call uses `ProcessBuilder(List<String>)` ([`GitCommandRunner.kt`](../../src/main/kotlin/de/hoennig/werkator/git/GitCommandRunner.kt)); no `Runtime.exec(String)`, no shell-string interpolation; the only `sh -c`/`bash -c` uses pass data as positional args.
- **Branch-name → filesystem** — always routed through [`ArtifactKeys.sanitize`](../../src/main/kotlin/de/hoennig/gittally/build/ArtifactKeys.kt) (`/``_`) plus a SHA suffix, so worktree/container/volume names cannot traverse. - **Branch-name → filesystem** — always routed through [`ArtifactKeys.sanitize`](../../src/main/kotlin/de/hoennig/werkator/build/ArtifactKeys.kt) (`/``_`) plus a SHA suffix, so worktree/container/volume names cannot traverse.
- **XSS** — no `th:utext` in any template; `gittally.js` builds all DOM via `createElement` + `textContent`/`dataset`, no `innerHTML`. - **XSS** — no `th:utext` in any template; `gittally.js` builds all DOM via `createElement` + `textContent`/`dataset`, no `innerHTML`.
- **SSRF** — the Gitea status proxy validates the commit against `[0-9a-fA-F]{7,40}` ([`StatusApiController.kt`](../../src/main/kotlin/de/hoennig/gittally/server/StatusApiController.kt)); the Gitea base URL/owner/repo/token come from config, not the request. - **SSRF** — the Gitea status proxy validates the commit against `[0-9a-fA-F]{7,40}` ([`StatusApiController.kt`](../../src/main/kotlin/de/hoennig/werkator/server/StatusApiController.kt)); the Gitea base URL/owner/repo/token come from config, not the request.
- **Deserialization** — Jackson JSON/YAML without polymorphic/default typing (no gadget-chain RCE); unreadable state files degrade to empty. - **Deserialization** — Jackson JSON/YAML without polymorphic/default typing (no gadget-chain RCE); unreadable state files degrade to empty.
- **Credential transport** — Gitea token sent as an `Authorization` header, never in a URL ([`GiteaClient.kt`](../../src/main/kotlin/de/hoennig/gittally/gitea/GiteaClient.kt)); git auth via env-based `GIT_ASKPASS` with a `0700` script containing no secrets, deleted in `finally`; TLS verification never disabled. - **Credential transport** — Gitea token sent as an `Authorization` header, never in a URL ([`GiteaClient.kt`](../../src/main/kotlin/de/hoennig/werkator/gitea/GiteaClient.kt)); git auth via env-based `GIT_ASKPASS` with a `0700` script containing no secrets, deleted in `finally`; TLS verification never disabled.
- **nginx/certbot** — `serverName`/`upstreamHost` validated against `[A-Za-z0-9][A-Za-z0-9.-]*` before substitution ([`NginxProxyManager.kt`](../../src/main/kotlin/de/hoennig/gittally/server/NginxProxyManager.kt)); ports range-checked. - **nginx/certbot** — `serverName`/`upstreamHost` validated against `[A-Za-z0-9][A-Za-z0-9.-]*` before substitution ([`NginxProxyManager.kt`](../../src/main/kotlin/de/hoennig/werkator/server/NginxProxyManager.kt)); ports range-checked.
@@ -35,8 +35,8 @@ So that critical load is visible at a glance.
##### Verified by ##### Verified by
- [UiViewsTest — "utilization highlights warn from 80% and crit from 90% of the total"](../../src/test/kotlin/de/hoennig/gittally/server/UiViewsTest.kt) - [UiViewsTest — "utilization highlights warn from 80% and crit from 90% of the total"](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
- [UiViewsTest — "only the used rows with a total get the critical highlighting"](../../src/test/kotlin/de/hoennig/gittally/server/UiViewsTest.kt) - [UiViewsTest — "only the used rows with a total get the critical highlighting"](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
#### Scenario#000.02: Unavailable metrics are never highlighted #### Scenario#000.02: Unavailable metrics are never highlighted
@@ -48,7 +48,7 @@ So that hosts without `/proc` (no metrics, `n/a` cells) render unchanged.
##### Verified by ##### Verified by
- [UiViewsTest — "utilization highlighting is off when a value or the total is unavailable"](../../src/test/kotlin/de/hoennig/gittally/server/UiViewsTest.kt) - [UiViewsTest — "utilization highlighting is off when a value or the total is unavailable"](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
## The Solution ## The Solution
@@ -37,7 +37,7 @@ So that a service restart never loses a build or marks its commit as failed.
##### Verified by ##### Verified by
- [BuildExecutorTest — "shutdown kills an executing build and records INTERRUPTED, not FAILED"](../../src/test/kotlin/de/hoennig/gittally/build/BuildExecutorTest.kt) - [BuildExecutorTest — "shutdown kills an executing build and records INTERRUPTED, not FAILED"](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt)
#### Scenario#000.02: A queued build stays PENDING over a shutdown #### Scenario#000.02: A queued build stays PENDING over a shutdown
@@ -50,7 +50,7 @@ So that queued builds survive a restart the same way executing builds do.
##### Verified by ##### Verified by
- [BuildExecutorTest — "a build still queued at shutdown stays PENDING for the startup recovery"](../../src/test/kotlin/de/hoennig/gittally/build/BuildExecutorTest.kt) - [BuildExecutorTest — "a build still queued at shutdown stays PENDING for the startup recovery"](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt)
#### Scenario#000.03: No failure status is posted to Gitea for an interrupted build #### Scenario#000.03: No failure status is posted to Gitea for an interrupted build
@@ -63,8 +63,8 @@ So that a commit does not turn red because of a service restart.
##### Verified by ##### Verified by
- [GiteaStateMappingTest](../../src/test/kotlin/de/hoennig/gittally/gitea/GiteaStateMappingTest.kt) - [GiteaStateMappingTest](../../src/test/kotlin/de/hoennig/werkator/gitea/GiteaStateMappingTest.kt)
- [GiteaClientTest — "maps every build status to the documented Gitea state"](../../src/test/kotlin/de/hoennig/gittally/gitea/GiteaClientTest.kt) - [GiteaClientTest — "maps every build status to the documented Gitea state"](../../src/test/kotlin/de/hoennig/werkator/gitea/GiteaClientTest.kt)
## The Solution ## The Solution
@@ -34,7 +34,7 @@ So that the cleanup is part of every GitTally installation instead of a manual s
##### Verified by ##### Verified by
- [InitCommandTest — "--systemd also generates the nightly Docker cleanup timer"](../../src/test/kotlin/de/hoennig/gittally/commands/InitCommandTest.kt) - [InitCommandTest — "--systemd also generates the nightly Docker cleanup timer"](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandTest.kt)
#### Scenario#000.02: The cleanup prunes containers and images but never volumes #### Scenario#000.02: The cleanup prunes containers and images but never volumes
@@ -48,8 +48,8 @@ So that nightly builds start from fresh images while the Gradle caches survive.
##### Verified by ##### Verified by
- [SystemdServiceFilesTest — "prune service cleans containers and images but never volumes"](../../src/test/kotlin/de/hoennig/gittally/commands/SystemdServiceFilesTest.kt) - [SystemdServiceFilesTest — "prune service cleans containers and images but never volumes"](../../src/test/kotlin/de/hoennig/werkator/commands/SystemdServiceFilesTest.kt)
- [SystemdServiceFilesTest — "prune timer fires nightly at 02:00 and catches up after downtime"](../../src/test/kotlin/de/hoennig/gittally/commands/SystemdServiceFilesTest.kt) - [SystemdServiceFilesTest — "prune timer fires nightly at 02:00 and catches up after downtime"](../../src/test/kotlin/de/hoennig/werkator/commands/SystemdServiceFilesTest.kt)
## The Solution ## The Solution