Files
werkator/docs/prs/2026-09-11-PR#51-blank-credentials-do-not-shadow.md
1f14f71fb3
werkator build succeeded after 01:28
config: a blank credential must not disable an inherited one (#51)
Five of six repositories on the production instance reported no commit
status to Gitea, silently. The cause was a placeholder: init wrote every
machine config with account: "" and token: "", the machine layer always
wins, and those empty values overrode the instance-wide defaults.git.
GiteaClient found no token and stayed quiet, because publishing a status
is non-fatal by design. The one repository that worked was the one whose
machine config happened to have no git block.

It reproduced itself, too: repo-add runs init, so every repository added
to an instance arrived with its inherited credentials already disabled.

Fixed at both ends. ConfigLoader drops a blank git.account/git.token from
a repository layer instead of letting it shadow the defaults below, and
names the file once — a credential is the one value that cannot mean
anything when empty, while an empty setting can be a choice, which
Scenario#51.04 pins down. That is also what repairs a running
installation: the next start inherits correctly with no file edited.

InitCommand writes the git block commented out, with the detected account
offered inside the comment. A template that asserts token: "" makes a
claim it has no basis for. loadFile then had to learn that a file of
nothing but comments is a layer that sets nothing — readValue answers
such a file with "No content to map" — which parseYaml has always done
for the branch layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #51
2026-09-11 06:28:37 +02:00

163 lines
8.2 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
On the production instance, five of six repositories reported no commit status to Gitea at all.
Nothing had failed, nothing was logged, and the builds themselves were green.
The cause is a placeholder.
`werkator init` wrote every repository's machine config with the credentials spelled out but empty:
```yaml
git:
account: ""
token: ""
```
The machine layer always wins, so those empty values overrode the instance-wide `defaults.git` of `~/.werkator.yml` (ADR 0009).
`GiteaClient.isEnabled` needs a non-blank `git.token`, found none, and stayed quiet — publishing a status is non-fatal by design.
The one repository that worked was the one whose machine config happened to have no `git` block.
The defect reproduces itself: `tools/remote werkator repo-add` runs `init`, so every repository added to an instance arrives with its inherited credentials already disabled.
Adding werkprobe is how it was found.
## Non-Goals
- No change to what a *setting* does when written empty — only credentials are treated as placeholders.
- No new configuration key, and no change to where credentials may be written.
- Repairing the machine configs of a running installation is deployment work, not code; this PR makes the next start repair itself.
## The Scenarios
### Feature: an unfilled credential does not disable an inherited one
#### Background
- The instance config `~/.werkator.yml` may carry a `defaults` block that sits *below* every repository layer (ADR 0009).
- A repository's own layers — project config, applied fragment, machine config — win over it, in that order.
- A *credential* is `git.account` or `git.token`.
#### Scenario#51.01: A blank credential falls through to the instance defaults
So that a repository added to an instance keeps the credentials it was meant to inherit.
- **Given** an instance config whose `defaults.git` carries an account and a token
- **and** a repository whose machine config writes `account: ""` and `token: ""`
- **When** the configuration is loaded
- **Then** the effective `git.account` and `git.token` are the instance's
- **and** the file is named once in a warning, because it still wants fixing
##### Verified by
- [ConfigLoaderTest "a blank credential in a repository layer does not shadow the instance defaults"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#51.02: A key written without a value counts as blank
So that `token:` and `token: ""` mean the same thing, since both are what an unfilled template looks like.
- **Given** an instance config whose `defaults.git` carries a token
- **and** a repository whose machine config writes `token:` with nothing behind it
- **When** the configuration is loaded
- **Then** the effective `git.token` is the instance's
##### Verified by
- [ConfigLoaderTest "a credential written without a value is as blank as an empty string"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#51.03: A credential that has a value still wins
So that a repository authenticating as someone else on this machine keeps doing so.
- **Given** an instance config whose `defaults.git` carries an account and a token
- **and** a repository whose machine config sets an account but leaves the token blank
- **When** the configuration is loaded
- **Then** the account is the repository's and the token is the instance's — the rule is per key
##### Verified by
- [ConfigLoaderTest "a credential that has a value still wins over the instance defaults"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#51.04: A blank setting is still a setting
So that "report nowhere" stays sayable.
- **Given** an instance config whose `defaults.gitea.baseUrl` names a Gitea
- **and** a repository whose machine config sets `gitea.baseUrl: ""`
- **When** the configuration is loaded
- **Then** the effective `gitea.baseUrl` is empty — only credentials are treated as placeholders
##### Verified by
- [ConfigLoaderTest "a blank value that is not a credential still overrides"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#51.05: Without an instance config nothing changes
So that a single-repository installation behaves exactly as before.
- **Given** no instance config
- **and** a repository whose machine config writes `token: ""`
- **When** the configuration is loaded
- **Then** the effective token is empty, and nothing is warned about — there is nothing below to shadow
##### Verified by
- [ConfigLoaderTest "without a home config a blank credential is left alone"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
### Feature: the template stops asserting what it does not know
#### Scenario#51.06: `init` writes the credentials commented out
So that a fresh repository inherits the instance credentials instead of disabling them.
- **Given** a repository with an https origin
- **When** `werkator init` runs
- **Then** the machine config contains the `git` block commented out, with the detected account offered inside the comment
- **and** loading that repository's configuration yields no `git` section at all
##### Verified by
- [InitCommandTest "creates config files with auto-detected values"](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandTest.kt)
- [InitCommandTest "detects account from https url"](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandTest.kt)
#### Scenario#51.07: A config file of nothing but comments is a layer that sets nothing
So that the file `init` now writes can be read at all, and an operator may comment every key out again.
- **Given** a machine config holding only comments
- **When** the configuration is loaded
- **Then** the other layers apply unchanged, and no error is raised
##### Verified by
- [ConfigLoaderTest "a config file of nothing but comments is a layer that sets nothing"](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
## The Solution
Two changes, at the two ends of the defect.
`ConfigLoader.withoutBlankCredentials` drops a blank `git.account`/`git.token` from each repository layer before merging, so it cannot shadow the `defaults.git` below it, and logs once per file — scoped like `checkVersion`, so the message names the file to fix.
Dropped rather than honoured because a credential is the one kind of value that cannot mean anything when empty: there is no build that wants to authenticate as nobody.
Everything else keeps the existing semantics, deliberately — an empty *setting* can be a choice, and Scenario#51.04 pins that down.
This is also what repairs a running installation: the next start inherits correctly without anyone editing a file.
`InitCommand` no longer writes the `git` block out with empty values; it writes it commented out, with the detected account offered inside the comment and a line saying why.
A template that asserts `token: ""` is a template that makes a claim it has no basis for.
`ConfigLoader.loadFile` then had to learn that a file of nothing but comments is a layer that sets nothing: `readValue` answers such a file with *"No content to map due to end-of-input"*, so it now goes through `readTree` and treats a missing node as an empty layer — the same thing `parseYaml` has always done for the branch layer.
## Open Questions
- The rule names `git.account` and `git.token` explicitly (`CREDENTIAL_KEYS`) rather than deriving "is a secret" from the schema.
A third credential would have to be added there by hand; the alternative — marking secrets in `WerkatorConfig` — is more machinery than two keys justify today.
## Additional Changes
- `docs/configuration.md`: the layering section now states the credential rule, and the machine-config section says that the block is written commented out and that a comment-only file is a valid layer.
- `.claude/skills/architecture/SKILL.md`: the configuration section names the rule.
## Follow-up PRs
- None. The machine configs of the running instance were repaired by hand when the defect was found; after this PR they would have repaired themselves.