Skip to content

fix(env): prune orphaned lock entries + idempotent UpsertEnv#176

Merged
fentas merged 2 commits into
mainfrom
fix/env-prune-orphaned-lock
Jun 25, 2026
Merged

fix(env): prune orphaned lock entries + idempotent UpsertEnv#176
fentas merged 2 commits into
mainfrom
fix/env-prune-orphaned-lock

Conversation

@fentas

@fentas fentas commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Problem (refined diagnosis — #174 fixed the wrong mode)

b verify was never clean for a user despite files being byte-identical to upstream. The real cause is duplicate lock env entries, not single-hash drift (which is why #174's auto-heal couldn't fix it).

Their b.lock had two blocks for the same repo:

  • git@github.com:kernpilot/lok8s#main — 223 files, configured in b.yaml
  • git@github.com:kernpilot/lok8s#kubeone — 115 files, NOT in b.yaml (a leftover from renaming the label kubeonemain)

They share 101 dests. For render.sh, #main records 61d915c4… (matches disk → ✓) while the orphaned #kubeone keeps a stale 1918e425… (→ ✗). Confirmed against their lock: jq '.envs[] | .ref,.label' → both present, only #main in config.

Why nothing healed it:

  • FindEnv/UpsertEnv key on the configured (ref,label) = #main, so the orphan is never re-synced or replaced → "(up to date)".
  • b verify iterates all blocks → reports #kubeone's stale hashes against files #main owns → 101 mismatches.
  • Nothing prunes a lock entry no longer in the config.

Fixes

  1. Prune (repair): updateEnvs drops lock env entries whose (ref,label) isn't in b.yaml, before writing the lock — keyed via the same gitcache.RefBase/RefLabel it writes with, so a configured env (even one filtered-out this run or up-to-date) is never pruned. The lock is rewritten on every b update, so a poisoned lock self-heals: orphan removed, verify goes clean. Emits a one-line notice of what was pruned.
  2. Idempotent UpsertEnv (prevention): replace the first (ref,label) match in place and drop any further duplicates, so a same-key dup collapses to one on write instead of leaving a stale twin.

Tests

  • TestUpdateEnvs_PrunesOrphanedLockEntries — orphan #kubeone removed while up-to-date #main is kept; fails without the prune (reproduces the 2-entry lock).
  • TestUpsertEnv_CollapsesDuplicates — pre-existing same-key dups collapse to one.

go build, go vet ./..., gofmt, full go test ./... all clean.

For kubehz-cluster: after this ships, a plain b update --envs-only prunes #kubeoneverify clean. (Pushed over HTTPS via the gh token; ssh-agent has no key this session.)

🤖 Generated with Claude Code

The real failure mode behind the kubehz-cluster "verify never clean" report was
duplicate lock env entries, not single-hash drift (so #174 couldn't heal it):
b.lock had two blocks for the same repo — #main (configured in b.yaml) and
#kubeone (NOT in b.yaml, a leftover from renaming the label). They shared 101
dests; for render.sh, #main recorded the correct hash (matches disk → verify ✓)
while the orphaned #kubeone kept a stale hash (→ verify ✗). FindEnv/UpsertEnv
key on the configured ref+label, so the orphan is never re-synced or replaced,
yet `b verify` iterates ALL blocks and reports its stale hashes forever.

Two fixes:

- prune (repair): updateEnvs now drops lock env entries whose (ref,label) is not
  in b.yaml before writing the lock — using the same RefBase/RefLabel keys it
  writes with, so a configured env (even one not synced this run, or up-to-date)
  is never pruned. Runs on every `b update` (the lock is rewritten regardless),
  so a poisoned lock self-heals: the orphan is removed, verify goes clean. Emits
  a one-line notice listing what was pruned.

- idempotent UpsertEnv (prevention): replace the first (ref,label) match in place
  and drop any further duplicates, so a same-key dup collapses to one on write
  instead of leaving a stale twin.

Tests: TestUpdateEnvs_PrunesOrphanedLockEntries (orphan #kubeone removed while
up-to-date #main is kept; fails without the prune) and
TestUpsertEnv_CollapsesDuplicates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes b verify reporting persistent env drift caused by stale/duplicate b.lock env entries by (1) pruning lock env entries that are no longer configured in b.yaml during b update, and (2) making lock env upserts idempotent by collapsing duplicate (ref,label) entries.

Changes:

  • Make Lock.UpsertEnv replace the first (ref,label) match and drop any further duplicates.
  • Add UpdateOptions.pruneOrphanedEnvs to remove lock env entries not present in the current config before writing b.lock.
  • Add regression tests covering orphan-pruning during update and duplicate-collapse behavior in UpsertEnv.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
pkg/lock/lock.go Makes env upsert idempotent by collapsing duplicate (ref,label) entries.
pkg/lock/lock_test.go Adds coverage for duplicate-collapse behavior in UpsertEnv.
pkg/cli/update.go Prunes orphaned lock env entries (not in b.yaml) before writing the lock.
pkg/cli/env_test.go Adds regression test ensuring b update prunes orphaned env lock entries even when the live env is up to date.

Comment thread pkg/cli/update.go
Copilot: pruneOrphanedEnvs only removed entries not in config. If b.lock held
two entries for the SAME configured (ref,label) and the env was up-to-date,
SyncEnv skips it → UpsertEnv never runs to collapse the twin → the lock is still
rewritten with both → b verify keeps flagging the stale duplicate.

The normalize pass now also collapses duplicates of a configured (ref,label),
keeping the first occurrence (the FindEnv-visible / up-to-date one). So a
rewritten lock is idempotently clean even when nothing synced this run — orphans
(not in b.yaml) and same-key twins are both dropped, with a per-reason notice.

Test: TestUpdateEnvs_DedupsConfiguredLockEntries (two #infra entries, env
up-to-date → collapses to one, keeps the good-hash first entry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@fentas

fentas commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Review loop complete — 2 rounds (Copilot + subagent). Round 1: also dedup same-key lock entries (Copilot). Round 2 clean (Copilot 0 new comments; subagent SHIP). Prunes orphaned + duplicate lock env entries so a poisoned lock self-heals on update; verify goes clean. Regression tests fail without the fix. build/vet/gofmt/test green. Merging.

@fentas fentas merged commit 44eb405 into main Jun 25, 2026
9 checks passed
@fentas fentas deleted the fix/env-prune-orphaned-lock branch June 25, 2026 20:40
fentas pushed a commit that referenced this pull request Jun 25, 2026
🤖 I have created a release *beep* *boop*
---


## [4.18.4](v4.18.3...v4.18.4)
(2026-06-25)


### Bug Fixes

* **env:** prune orphaned lock entries + idempotent UpsertEnv
([#176](#176))
([44eb405](44eb405))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants