fix(config): keep an explicit false/0/"" from config.yaml - #632
Conversation
cleanenv applies env-default after the YAML decode to any field still at its zero value, so an explicit `otel.traces.enabled: false` or `sample_rate: 0` came back as the default. Defaults now live in one Go function that Load starts from before the decode; env-default is gone. Tests load through config.Load for every affected key, refuse an env-default tag, and pin configuration.mdx's defaults to defaults(). Fixes #631. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📚 Docs preview is live → https://6e1fc77c-wavehouse-docs.wave-rf.workers.dev
|
Code Coverage OverviewLanguages: Go GoThe overall line coverage in commit 40b5038 in the Show a line coverage summary of the most impacted files.
Updated |
# Conflicts: # CHANGELOG.md
Brings in #647, #632, #616 and #655. One conflict, in config.go: main dropped the Cache struct's env-default tag while this branch moved Cache into backends.go; kept the move. #632 removed every env-default tag. The four *.backend fields and cache.l1_max_cost this branch declares in backends.go still carried theirs, so an explicit `backend: ""` would have become the default. Their defaults now live in defaults(); an empty backend, which Validate refuses, is pinned next to server.port's zero as a refusal; the docs test converts a documented default to a named string type such as MQBackend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in main via feat/coord-leases: #618's squash, #619, #632, #616, #655 and #647. Per #632, the roles default moves from its env-default tag into defaults(): an explicit `roles: []` now reaches Validate (a refusedZeros entry pins it), and the doc-defaults test parses the roles cell as a comma-separated list. instance_id's documented default is *(empty)*, the value in defaults(); Load resolves it to <hostname>-<8 hex>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main now carries #612 and #618 as squashes, plus #632, #622, #627, #615, #619, #647, #616, #655 and #623. The merge was resolved against the pre-squash #618 head (f129d57) as its base, so main's version wins for everything this stack does not own and only the cache stack's changes (#614, #621, #626 as merged here, and this PR) are re-applied on top. Warnings keeps main's api-role gate for the cache.redis warnings too: a split's Deployments differ only in roles, so the API's cover the others'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aS7rLrH1RKkUMem7X4ckd
Adopt #632's convention for the cache.redis block: its seven non-zero defaults move from env-default tags into defaults(), each gets a zero case, and the configuration.mdx rows use *(empty)* for an empty default. The doc-defaults test learns to read a duration and an empty list. With the file's zeros now kept, compress_min_bytes needs no -1: 0 means never compress, the backend's own meaning, from the file and the environment alike, and wire.go passes it through unchanged. A negative value refuses boot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aS7rLrH1RKkUMem7X4ckd
Since #632 an env-default tag is refused: cleanenv re-applies it to a YAML zero, so `mq.nats.partitions: 0` came back 1. The block's defaults now come from defaultMQNATS() inside defaults(), each non-zero one has a zeroCases entry (the block is validated only under backend=nats, so its zeros load as written), the docs test reads a derived default such as `<prefix>_coord` and *(none)* as the zero, and the TLS pair gets a row per key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyrXjhR7iDg33paioLQHFq
Fixes #631.
What was wrong
Each boot-config default lived in a cleanenv
env-defaulttag. cleanenv applies those after the YAML decode, to any field that is still at its zero value. So it could not tell a key the file set tofalse/0/""apart from a key the file left out.otel.traces.enabled: falseloaded astrue, andsample_rate: 0loaded as1.0. Nothing reported it.The fix (direction 1 from the issue)
internal/config/config.go: a new unexporteddefaults()is now the only place defaults are defined.Loadstarts from it, cleanenv decodes the YAML over it, and then applies theWH_*variables. Precedence is still env > YAML > default. Allenv-defaulttags are gone. The env tags are unchanged, sounboundEnv,rejectUnknownKeysandTestEnvSettingsDir_MatchesStructTagbehave exactly as before.Behaviour change
A zero value you write in
config.yamlnow takes effect. If your file relied on the bug:sample_rate: 0now exports no traces, or no DEBUG/INFO logs. Before, it silently exported everything.enabled: falseis now off.shutdown_timeout: 0now skips the drain.cache.l1_max_cost: 0now refuses boot withcache init: MaxCost can't be zero.server.port: 0anddata_dir: ""also refuse boot. An emptyprometheus.pathrefuses boot when Prometheus is enabled.To get the default back, delete the key. Env vars already honoured an explicit zero, so they are unchanged. This is described under Fixed in the CHANGELOG.
Tests (
internal/config/defaults_test.go, all throughconfig.Load)TestLoad_YAMLZeroIsKept: for each key in the issue's table, a YAML false/0/"" comes back unchanged. This is the case that fails if defaults are applied again after the decode. I confirmed it: with the old loader it fails, and so doTestLoad_IssueReproFile,TestLoad_YAMLZeroPortIsRefusedandTestConfig_NoEnvDefaultTags.TestLoad_IssueReproFile: loads the issue's repro file as written.TestLoad_AbsentKeyGetsDefault: when the file exists but leaves a key out, that key gets its default.TestLoad_EnvWinsOverYAMLZeroAndDefault: env wins over a YAML zero, an env zero wins over a YAML value, and an env zero wins over the default when there is no file.TestZeroCases_CoverEveryNonZeroDefault: a new non-zero default without regression coverage fails this test.TestConfig_NoEnvDefaultTags: refuses anyenv-defaulttag.TestDocs_DefaultsMatchCode: each Config field has exactly one row inconfiguration.mdx. The row must name the field's env var, and its Default column must parse to the value indefaults(). Doc rows with no matching field also fail.Deliberately left out
-1sentinel forcache.redis.compress_min_bytes. Once this lands,0could mean "never compress". That change belongs to feat(config): cache.backend=redis selects the shared cache #630's owner.Validatecheck forcache.l1_max_cost <= 0. Ristretto already refuses 0 at boot, and a new check would break every test that builds a Config literal without a cache block.Reviewers
pre-push-reviewer(opus), at 252ef7b: ship_it, with 0 MUST, 0 SHOULD and 0 MAY findings. It read cleanenv v1.5.0's source to confirm the precedence. It also checked that no shipped config file (config.yaml,tests/e2e/fixtures/config.yaml,deployments/compose/standalone.yaml) relied on the bug.docs-reviewer(opus), at 252ef7b: ship_it, with 0 findings.Gate gap #454: reviewer markers land in the main checkout, not this worktree.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL