Read-only integrity check, the safety net for the stage-2 store migration.
Default mode (current project) cross-checks $PWD/.secrets.json against the
store both ways — declared-but-missing blobs and orphaned blobs (no manifest
entry) — and decrypt-tests every dotenv + external blob with the current key,
streaming plaintext to /dev/null so nothing is ever written to disk. `verify
--all` decrypt-tests every blob in every project (integrity only; the store
carries no manifests, so consistency can't be checked store-wide). Both
recurse the whole project tree (find -type f), the same walk rekey/list use,
so nested manifest blobs are covered. Exits non-zero on any finding so it can
gate `migrate --finalize` and CI.
12 bats tests (clean, nested+external, missing blob, decrypt failure, orphan,
missing external, no-manifest die, symlink refusal, --all clean/corrupt/orphan,
nested decrypt failure). Full suite 205/205. bash 3.2 clean.
Pre-landing review (testing + checklist specialists, reproduced) caught a
data-loss bug: cmd_rekey's decrypt/re-encrypt globs were non-recursive and
only special-cased external/. Nested manifest dotenv blobs
(<project>/<relpath>.age, new this branch) were never visited, so after a key
rotation they stayed encrypted under the discarded old key = permanently
undecryptable. cmd_list had the same blind spot (cosmetic: nested entries
invisible in listings).
Both now walk the entire project tree with `find -type f` (bash 3.2 safe,
includes dotfiles natively), unifying top-level / nested / external blobs into
one recursive pass and dropping the now-redundant external/ special-casing.
Regression tests: nested-blob rekey round-trip (survives rotation) + list
shows nested entry. Full suite 193/193.
- pull with .secrets.json restores exactly the declared entries (nested
paths get mkdir -p); stray store blobs are not restored
- dotenv rail re-runs at restore time: unsafe entries warn+skip (pull
never dies on one bad entry), missing blobs warn with a directed hint
- empty manifest = warn no-op instead of a confusing 'not found' death
- manifest-less projects keep the legacy glob pull verbatim
- .secrets.json external[] drives push/pull: type 'properties' (alias of
gradle-properties; blob suffix stays legacy-compatible in stage 1) and
type 'file'; same charset rails as the legacy parser
- push absorbs uncovered .secrets-files entries into the manifest
(idempotent, gradle-properties → properties) with a delete hint
- pull: manifest wins entirely; a coexisting .secrets-files warns as
superseded instead of being silently ignored
- basename rail generalized: properties targets must end '.properties'
(was exact 'gradle.properties') — rc files/gitconfig still blocked;
EGB-531 wrong-basename test updated for the sanctioned change
- push syncs FROM the manifest; v1 store layout unchanged (nested
entries land at <project>/<relpath>.age, same shape -w always used)
- discovery (root globs + quiet package.json workspace re-scan when a
manifest exists) feeds the manifest as a generator; new files auto-add
with ==> notice + undo guidance
- options.autoAdd committed toggle (default ON when absent); explicit
false warns on undeclared files instead of enrolling them
- push --frozen: declared-only for one invocation; push --dry-run:
reports would-add/would-sync, touches nothing
- bootstrap ordering: manifest written only after >=1 blob encrypts
- declared-but-missing warns and continues; unsafe manifest path dies
- jq // falsy gotcha: explicit autoAdd:false compared directly