werkator build succeeded after 01:28
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
163 lines
8.2 KiB
Markdown
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.
|