diff --git a/secrets b/secrets index 2328fdc..97676b8 100755 --- a/secrets +++ b/secrets @@ -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 diff --git a/test/migrate.bats b/test/migrate.bats index 35102f6..84f7ec9 100644 --- a/test/migrate.bats +++ b/test/migrate.bats @@ -241,6 +241,21 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' > [ ! -f "$SECRETS_DIR/.secrets-format" ] } +@test "verify is green during the migration window (v2 twin is not a spurious orphan)" { + # Regression: after copy-forward the store is still v1, so verify computed the + # external blob path as .gradle-properties.age and flagged the .properties.age + # twin as an orphan, failing verify mid-migration. The orphan set now accounts + # for both suffix forms. + make_v1_store + m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' + create_project_dir windowverify + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files + "$SECRETS_BIN" push windowverify >/dev/null 2>&1 + "$SECRETS_BIN" migrate >/dev/null 2>&1 + run "$SECRETS_BIN" verify windowverify + [ "$status" -eq 0 ] +} + @test "v1 client still reads during the migration window (after copy-forward, before finalize)" { make_v1_store m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'