fix(diff): compare non-secret env vars against fresh value, not stale real_value (#78) #79

Merged
dan-claude-bot merged 1 commit from fix/env-diff-real-value into main 2026-07-16 15:43:03 +00:00
dan-claude-bot commented 2026-07-16 14:53:13 +00:00 (Migrated from github.com)

Fixes #78.

What

cast diff re-proposed env vars that were updated in place and are genuinely correct on the box. During the incubator prod cutover (2026-07-16), five flags flipped false→true, were applied, and redeployed — yet every subsequent cast diff --env prod still listed all five as env … : change. Vars that were only ever created (NODE_ENV, the derived base-URLs) did not re-propose. A false drift that never clears also masks real drift.

Root cause

fetchEnv collapsed each live var to real_value ?? value. Coolify leaves the stored real_value stale after an in-place PATCH of value (a redeploy does not refresh it), so the diff compared the stale "false" against the manifest's "true". Created-once vars have a fresh real_value, which is exactly why only the flipped-in-place vars re-proposed.

Fix (cast-side, per-var — issue's preferred direction)

The real_value ?? value preference is deliberate for secrets (value is masked to a plain token, so real_value is the only plaintext). So the choice is made per var, not blanket-switched:

  • fetchEnv now carries both forms through as LiveEnvVar { value, realValue } instead of collapsing at read time.
  • diffEnv picks per the desired side's secret flag it already has: non-secret → value (always fresh), secret → realValue ?? value (unchanged).
  • capture and draft, which want the decrypted plaintext and compare against no manifest literal, keep the old flattening via a small flattenEnv helper — behavior unchanged.

Tests

  • A non-secret var flipped in place with a stale realValue reads clean (the exact prod-cutover reproduction).
  • Regression guard: a secret whose value is masked still diffs via realValue, so a genuine rotation is still caught.

npm run check clean · 511 tests pass.

🤖 Generated with Claude Code

Fixes #78. ## What `cast diff` re-proposed env vars that were **updated in place** and are genuinely correct on the box. During the incubator prod cutover (2026-07-16), five flags flipped false→true, were applied, and redeployed — yet every subsequent `cast diff --env prod` still listed all five as `env … : change`. Vars that were only ever *created* (`NODE_ENV`, the derived base-URLs) did not re-propose. A false drift that never clears also **masks real drift**. ## Root cause `fetchEnv` collapsed each live var to `real_value ?? value`. Coolify leaves the stored **`real_value` stale after an in-place PATCH** of `value` (a redeploy does not refresh it), so the diff compared the stale `"false"` against the manifest's `"true"`. Created-once vars have a fresh `real_value`, which is exactly why only the flipped-in-place vars re-proposed. ## Fix (cast-side, per-var — issue's preferred direction) The `real_value ?? value` preference is deliberate for **secrets** (`value` is masked to a plain token, so `real_value` is the only plaintext). So the choice is made **per var**, not blanket-switched: - `fetchEnv` now carries both forms through as `LiveEnvVar { value, realValue }` instead of collapsing at read time. - `diffEnv` picks per the desired side's `secret` flag it already has: **non-secret → `value`** (always fresh), **secret → `realValue ?? value`** (unchanged). - `capture` and `draft`, which want the decrypted plaintext and compare against no manifest literal, keep the old flattening via a small `flattenEnv` helper — behavior unchanged. ## Tests - A non-secret var flipped in place with a stale `realValue` reads **clean** (the exact prod-cutover reproduction). - Regression guard: a **secret** whose `value` is masked still diffs via `realValue`, so a genuine rotation is still caught. `npm run check` clean · 511 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#79
No description provided.