From 469b08203b12dc7203f11ed3fdcdeeca58baad46 Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Mon, 8 Jun 2026 07:34:55 -0700 Subject: [PATCH] feat: secrets migrate --status surveys per-project v2 readiness (EGB-710) --- secrets | 51 +++++++++++++++++++++++++++++++++++++++++++---- test/migrate.bats | 34 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/secrets b/secrets index e4b86d3..6f4158a 100755 --- a/secrets +++ b/secrets @@ -2186,6 +2186,46 @@ _migrate_project() { # Store-wide finalize: the only destructive step. Refuses unless verify --all # is green and every v1 properties blob has a v2 twin. Cuts a recovery tag, # stamps the marker, then drops v1 blobs. +# Read-only survey: walk every project dir in the store and report each one's +# v2-readiness from the blobs on disk (no manifest, no decryption). Exits +# non-zero when any v1 properties blob lacks a v2 twin (i.e. the store is not +# yet finalize-ready) so it can gate scripting, mirroring `verify`'s posture. +_migrate_status() { + if [ "$(_store_format)" = "2" ]; then + info "Store format: v2 (finalized) — nothing to migrate." + return 0 + fi + echo "Store format: v1 (not finalized). Per-project migration status:" + local any_untwinned=0 dir project v1 untwinned f new + for dir in "$SECRETS_DIR"/*/; do + [ -d "$dir" ] || continue + project=$(basename "$dir") + case "$project" in .*) continue ;; esac + v1=0; untwinned=0 + while IFS= read -r f; do + [ -f "$f" ] || continue + v1=$((v1 + 1)) + new="${f%.gradle-properties.age}.properties.age" + [ -f "$new" ] || untwinned=$((untwinned + 1)) + done < <(find "$dir" -type f -name '*.gradle-properties.age' 2>/dev/null) + if [ "$v1" -eq 0 ]; then + echo " $project: v2-ready (no v1 properties blobs)" + elif [ "$untwinned" -eq 0 ]; then + echo " $project: migrated ($v1 v1 blob(s), all twinned)" + else + echo " $project: NEEDS MIGRATE ($untwinned of $v1 v1 blob(s) un-twinned) — cd into the project and run 'secrets migrate'" + any_untwinned=1 + fi + done + echo + if [ "$any_untwinned" -eq 1 ]; then + echo "Not finalize-ready: migrate the projects marked NEEDS MIGRATE, then run 'secrets migrate --finalize'." + return 1 + fi + echo "Finalize-ready: every v1 properties blob has a v2 twin. Run 'secrets migrate --finalize' once every machine is upgraded." + return 0 +} + _migrate_finalize() { local force="$1" check_key @@ -2260,17 +2300,20 @@ $untwinned cd into each project and run 'secrets migrate', then re-run 'secrets cmd_migrate() { resolve_store check_initialized - local dry_run=false finalize=false force=false + local dry_run=false finalize=false force=false status=false while [ $# -gt 0 ]; do case "$1" in --dry-run) dry_run=true; shift ;; --finalize) finalize=true; shift ;; + --status) status=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)." ;; + -*) die "Unknown migrate flag: $1. Usage: secrets migrate [--dry-run | --status | --finalize] [--yes]" ;; + *) die "migrate takes no project argument. Run it from inside a project (copy-forward), or use --status / --finalize (store-wide)." ;; esac done - if [ "$finalize" = true ]; then + if [ "$status" = true ]; then + _migrate_status + elif [ "$finalize" = true ]; then _migrate_finalize "$force" else _migrate_project "$dry_run" diff --git a/test/migrate.bats b/test/migrate.bats index f3d2daa..9441bca 100644 --- a/test/migrate.bats +++ b/test/migrate.bats @@ -346,6 +346,40 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' > [[ "$output" == *"already format v2"* ]] || false } +@test "migrate --status flags a project that needs migrating" { + make_v1_store + m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' + create_project_dir needsmig + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files + "$SECRETS_BIN" push needsmig >/dev/null 2>&1 # v1 blob, no twin yet + run "$SECRETS_BIN" migrate --status + [ "$status" -ne 0 ] # not finalize-ready + [[ "$output" == *"needsmig"* ]] || false + [[ "$output" == *"NEEDS MIGRATE"* ]] || false + [[ "$output" == *"Not finalize-ready"* ]] || false +} + +@test "migrate --status reports finalize-ready once every blob is twinned" { + make_v1_store + m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' + create_project_dir readymig + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files + "$SECRETS_BIN" push readymig >/dev/null 2>&1 + "$SECRETS_BIN" migrate >/dev/null 2>&1 # create the twin + run "$SECRETS_BIN" migrate --status + [ "$status" -eq 0 ] + [[ "$output" == *"Finalize-ready"* ]] || false +} + +@test "migrate --status on an already-v2 store says nothing to do" { + init_with_remote + create_project_dir v2status + run "$SECRETS_BIN" migrate --status + [ "$status" -eq 0 ] + [[ "$output" == *"v2"* ]] || false + [[ "$output" == *"nothing to migrate"* ]] || false +} + @test "_store_format reads a garbage marker as v1 (strict parse)" { make_v1_store create_project_dir garbagemarker