feat: secrets migrate --status surveys per-project v2 readiness (EGB-710)

This commit is contained in:
Brian Majewski 2026-06-08 07:34:55 -07:00
parent e75627deff
commit 469b08203b
2 changed files with 81 additions and 4 deletions

51
secrets
View file

@ -2186,6 +2186,46 @@ _migrate_project() {
# Store-wide finalize: the only destructive step. Refuses unless verify --all # 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, # is green and every v1 properties blob has a v2 twin. Cuts a recovery tag,
# stamps the marker, then drops v1 blobs. # 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() { _migrate_finalize() {
local force="$1" local force="$1"
check_key check_key
@ -2260,17 +2300,20 @@ $untwinned cd into each project and run 'secrets migrate', then re-run 'secrets
cmd_migrate() { cmd_migrate() {
resolve_store resolve_store
check_initialized check_initialized
local dry_run=false finalize=false force=false local dry_run=false finalize=false force=false status=false
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--dry-run) dry_run=true; shift ;; --dry-run) dry_run=true; shift ;;
--finalize) finalize=true; shift ;; --finalize) finalize=true; shift ;;
--status) status=true; shift ;;
--yes) force=true; shift ;; --yes) force=true; shift ;;
-*) die "Unknown migrate flag: $1. Usage: secrets migrate [--dry-run | --finalize] [--yes]" ;; -*) 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 --finalize (store-wide)." ;; *) die "migrate takes no project argument. Run it from inside a project (copy-forward), or use --status / --finalize (store-wide)." ;;
esac esac
done done
if [ "$finalize" = true ]; then if [ "$status" = true ]; then
_migrate_status
elif [ "$finalize" = true ]; then
_migrate_finalize "$force" _migrate_finalize "$force"
else else
_migrate_project "$dry_run" _migrate_project "$dry_run"

View file

@ -346,6 +346,40 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' >
[[ "$output" == *"already format v2"* ]] || false [[ "$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)" { @test "_store_format reads a garbage marker as v1 (strict parse)" {
make_v1_store make_v1_store
create_project_dir garbagemarker create_project_dir garbagemarker