fix: pre-landing review for store-format-v2 (verify-window orphan, copy-forward push, --force, comment)

Pre-landing review (1 critical, 4 informational):
- CRITICAL: per-project `secrets verify` during the migration window flagged
  the freshly-written v2 twin as a spurious orphan and exited 1 (store still
  reads v1, so verify's expected set only held the .gradle-properties.age name).
  Breaks the documented migrate->verify->finalize workflow and CI. Fix: verify's
  orphan set now accounts for BOTH suffix forms of a properties external, so the
  twin is never a false orphan mid-migration. Regression test added.
- Copy-forward now pushes the twins (mirrors push/rekey) so a --finalize on
  another machine sees them; previously twins were local-only until finalize,
  a multi-machine footgun.
- Dropped the undocumented `--force` alias (keep `--yes`).
- Clarified the EGB-700 comment (which-format line, folded into EGB-703).

Deferred to EGB-701: the two finalize find-walks over *.gradle-properties.age
could collapse to one pass. Full suite 236/236.
This commit is contained in:
Brian Majewski 2026-06-07 16:47:17 -07:00
parent 5521d978a3
commit c10e89fb51
2 changed files with 27 additions and 4 deletions

16
secrets
View file

@ -1908,8 +1908,8 @@ cmd_which() {
resolve_store
echo "store: $SECRETS_DIR"
echo "source: $STORE_SOURCE"
# EGB-700: surface the store format so users can tell v1 from v2 during the
# migration window. A v1 store is a legacy store with no format marker.
# EGB-700 (folded into EGB-703): surface the store format so users can tell
# v1 from v2 during the migration window. v1 = legacy store, no format marker.
echo "format: v$(_store_format)"
# v2 manifest (.secrets.json): validate and summarize. Validation here
@ -2051,7 +2051,12 @@ _verify_project() {
[ -n "$etype" ] || continue
slug=$(_secrets_files_slug "$epath")
erel="external/$slug.$(_external_blob_suffix "$etype").age"
expected="$expected$erel"$'\n'
# Account for BOTH the v1 and v2 suffix forms in the orphan set. During the
# migration window (after copy-forward, before --finalize) the v2 twin
# coexists with the v1 blob; neither should read as an orphan whichever
# format the store currently reports. (file's two forms are identical.)
expected="${expected}external/$slug.$etype.age"$'\n'
[ "$etype" = "gradle-properties" ] && expected="${expected}external/$slug.properties.age"$'\n'
eblob="$pdir/$erel"
if [ ! -f "$eblob" ]; then
echo "FINDING: external '$epath' ($etype) is declared but has no blob in the store ($project/$erel missing). Run 'secrets push'." >&2
@ -2176,6 +2181,9 @@ _migrate_project() {
ensure_store_protections
git -C "$SECRETS_DIR" add -A
git -C "$SECRETS_DIR" commit -m "migrate: copy-forward v2 twins for $project" >/dev/null 2>&1 || true
# Push the twins so a --finalize on another machine sees them (finalize
# refuses any v1 blob without a twin). Mirrors push/rekey's push behavior.
git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1 && git -C "$SECRETS_DIR" push >/dev/null 2>&1 || true
info "Copy-forward for '$project': $moved new v2 twin(s), $already already present. v1 blobs kept (non-destructive). Run 'secrets migrate --finalize' once every project is migrated and every machine is upgraded."
}
@ -2261,7 +2269,7 @@ cmd_migrate() {
case "$1" in
--dry-run) dry_run=true; shift ;;
--finalize) finalize=true; shift ;;
--yes|--force) force=true; shift ;;
--yes) force=true; shift ;;
-*) die "Unknown migrate flag: $1. Usage: secrets migrate [--dry-run | --finalize] [--yes]" ;;
*) die "migrate takes no project argument. Run it from inside a project (copy-forward) or use --finalize (store-wide)." ;;
esac