From e21c94d0e7e5c37f50dd2d9a46ee8bae2ba2db78 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Mon, 31 Aug 2026 07:34:39 +0200 Subject: [PATCH] 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 --- ...-08-PR#000-age-based-artifact-retention.md | 12 +++--- ...PR#000-build-only-pull-request-branches.md | 14 +++---- ...-artifact-links-for-latest-green-builds.md | 20 +++++----- ...6-07-08-PR#000-security-audit-hardening.md | 40 +++++++++---------- ...0-PR#000-highlight-critical-utilization.md | 6 +-- ...000-interrupt-builds-on-server-shutdown.md | 8 ++-- ...08-10-PR#000-nightly-docker-prune-timer.md | 6 +-- 7 files changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/prs/2026-07-08-PR#000-age-based-artifact-retention.md b/docs/prs/2026-07-08-PR#000-age-based-artifact-retention.md index 029b679..ad7c232 100644 --- a/docs/prs/2026-07-08-PR#000-age-based-artifact-retention.md +++ b/docs/prs/2026-07-08-PR#000-age-based-artifact-retention.md @@ -36,8 +36,8 @@ So that stale logs and reports do not stay on disk indefinitely. ##### Verified by -- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) -- [WatcherTest](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) +- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt) +- [WatcherTest](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt) #### 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 -- [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 @@ -62,7 +62,7 @@ So that a dormant branch keeps its last build status visible, matching the legac ##### 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 @@ -75,7 +75,7 @@ So that the permanent `/branches//…` artifact links stay valid whi ##### 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 @@ -87,7 +87,7 @@ So that existing installations are unaffected by the new key. ##### 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 diff --git a/docs/prs/2026-07-08-PR#000-build-only-pull-request-branches.md b/docs/prs/2026-07-08-PR#000-build-only-pull-request-branches.md index b3ee73b..3557b95 100644 --- a/docs/prs/2026-07-08-PR#000-build-only-pull-request-branches.md +++ b/docs/prs/2026-07-08-PR#000-build-only-pull-request-branches.md @@ -39,7 +39,7 @@ So that pull-request branches get their commit status verified as before. ##### 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! @@ -54,7 +54,7 @@ So that work-in-progress branches do not consume build capacity. ##### 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! @@ -66,7 +66,7 @@ So that GitTally needs no Gitea credentials for this feature. ##### 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! @@ -78,7 +78,7 @@ So that existing installations see no new network traffic. ##### 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! @@ -91,7 +91,7 @@ So that permanent branches like `main` keep building after merges, whose merge c ##### 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! @@ -105,7 +105,7 @@ So that scheduled rebuilds follow the same policy as push-triggered builds. ##### 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! @@ -119,7 +119,7 @@ So that the same committed configuration works on environments whose origin is p ##### 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 diff --git a/docs/prs/2026-07-08-PR#000-permanent-artifact-links-for-latest-green-builds.md b/docs/prs/2026-07-08-PR#000-permanent-artifact-links-for-latest-green-builds.md index 23998f9..cd603bc 100644 --- a/docs/prs/2026-07-08-PR#000-permanent-artifact-links-for-latest-green-builds.md +++ b/docs/prs/2026-07-08-PR#000-permanent-artifact-links-for-latest-green-builds.md @@ -34,8 +34,8 @@ So that links in READMEs, wikis, and bookmarks stay valid across new builds. ##### Verified by -- [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/ArtifactFileControllerTest.kt) -- [BranchPermalinksTest](../../src/test/kotlin/de/hoennig/gittally/server/BranchPermalinksTest.kt) +- [ArtifactFileControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerTest.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 @@ -49,7 +49,7 @@ So that legacy-style report links such as `/branches/main/reports/build/doc/` wo ##### 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 @@ -63,8 +63,8 @@ So that users can browse the latest green artifacts from one stable bookmark. ##### Verified by -- [UiControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt) -- [PermanentBranchRoutesTest](../../src/test/kotlin/de/hoennig/gittally/server/PermanentBranchRoutesTest.kt) (the `/branches`, `/branches/`, and `/branches//` routes coexist) +- [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) +- [PermanentBranchRoutesTest](../../src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt) (the `/branches`, `/branches/`, and `/branches//` routes coexist) #### 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 -- [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 @@ -94,8 +94,8 @@ So that a permanent link stays valid while newer builds fail, as long as the bra ##### Verified by -- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) -- [WatcherTest](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt) +- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt) +- [WatcherTest](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt) #### 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 -- [BranchListingTest](../../src/test/kotlin/de/hoennig/gittally/server/BranchListingTest.kt) -- [UiControllerTest](../../src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt) +- [BranchListingTest](../../src/test/kotlin/de/hoennig/werkator/server/BranchListingTest.kt) +- [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) ## The Solution diff --git a/docs/prs/2026-07-08-PR#000-security-audit-hardening.md b/docs/prs/2026-07-08-PR#000-security-audit-hardening.md index 4d713e6..432f579 100644 --- a/docs/prs/2026-07-08-PR#000-security-audit-hardening.md +++ b/docs/prs/2026-07-08-PR#000-security-audit-hardening.md @@ -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 -- [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. - **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. +- [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/werkator/SecretFiles.kt), which sets the mode as a file attribute at creation. **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). @@ -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 -- [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. So the `` 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. @@ -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. **Background.** -Every server-rendered page embeds the live control token in `` 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 `` 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. 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. @@ -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 -- [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.** 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` -- [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. - [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 -- [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. 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. @@ -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.** -- [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] 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`. - [ ] **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 :.gittally.yml`); out of scope here. **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. 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. @@ -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` -- [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. **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 -- [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.** `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 -- [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 -- `). +- [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 -- `). **Done for `checkout` and `fetchBranch`.** `resetHardToOrigin` keeps its plain form: `git reset --hard -- ` is rejected (`fatal: Cannot do hard reset with paths`), and its argument is already prefixed with `origin/`, so it can never start with `-`. **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 -- [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. - **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. +- [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/werkator/SecretFiles.kt), the setup script by writing the YAML in a `umask 077` subshell instead of `chmod`-ing afterwards. **Background.** 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. -- **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`. -- **Command injection** — every git/docker/certbot/nginx call uses `ProcessBuilder(List)` ([`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. -- **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. +- **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)` ([`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/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`. -- **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. -- **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. -- **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. +- **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/werkator/server/NginxProxyManager.kt)); ports range-checked. diff --git a/docs/prs/2026-08-10-PR#000-highlight-critical-utilization.md b/docs/prs/2026-08-10-PR#000-highlight-critical-utilization.md index 7f5abf4..a82ba8f 100644 --- a/docs/prs/2026-08-10-PR#000-highlight-critical-utilization.md +++ b/docs/prs/2026-08-10-PR#000-highlight-critical-utilization.md @@ -35,8 +35,8 @@ So that critical load is visible at a glance. ##### 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 — "only the used rows with a total get the critical highlighting"](../../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/werkator/server/UiViewsTest.kt) #### 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 -- [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 diff --git a/docs/prs/2026-08-10-PR#000-interrupt-builds-on-server-shutdown.md b/docs/prs/2026-08-10-PR#000-interrupt-builds-on-server-shutdown.md index 5d2a972..0334126 100644 --- a/docs/prs/2026-08-10-PR#000-interrupt-builds-on-server-shutdown.md +++ b/docs/prs/2026-08-10-PR#000-interrupt-builds-on-server-shutdown.md @@ -37,7 +37,7 @@ So that a service restart never loses a build or marks its commit as failed. ##### 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 @@ -50,7 +50,7 @@ So that queued builds survive a restart the same way executing builds do. ##### 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 @@ -63,8 +63,8 @@ So that a commit does not turn red because of a service restart. ##### Verified by -- [GiteaStateMappingTest](../../src/test/kotlin/de/hoennig/gittally/gitea/GiteaStateMappingTest.kt) -- [GiteaClientTest — "maps every build status to the documented Gitea state"](../../src/test/kotlin/de/hoennig/gittally/gitea/GiteaClientTest.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/werkator/gitea/GiteaClientTest.kt) ## The Solution diff --git a/docs/prs/2026-08-10-PR#000-nightly-docker-prune-timer.md b/docs/prs/2026-08-10-PR#000-nightly-docker-prune-timer.md index 3e02b4f..78af9fa 100644 --- a/docs/prs/2026-08-10-PR#000-nightly-docker-prune-timer.md +++ b/docs/prs/2026-08-10-PR#000-nightly-docker-prune-timer.md @@ -34,7 +34,7 @@ So that the cleanup is part of every GitTally installation instead of a manual s ##### 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 @@ -48,8 +48,8 @@ So that nightly builds start from fresh images while the Gradle caches survive. ##### Verified by -- [SystemdServiceFilesTest — "prune service cleans containers and images but never volumes"](../../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/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/werkator/commands/SystemdServiceFilesTest.kt) ## The Solution