feat: read-resolver tries both external suffixes (additive v2, EGB-712)

This commit is contained in:
Brian Majewski 2026-06-08 10:08:41 -07:00
parent ee4ea413ef
commit 2866e5f4b1
2 changed files with 46 additions and 7 deletions

38
secrets
View file

@ -554,6 +554,31 @@ _external_blob_suffix() {
fi
}
# Resolve the on-disk path of an external blob for READING. Tries the v2 suffix
# (.properties.age) first, then falls back to the v1 (.gradle-properties.age) for
# `properties` externals, so an upgraded client finds the blob whichever format
# wrote it (additive v2 — EGB-712). `file` externals share one suffix in both
# formats. Echoes the path of the blob that exists; if neither exists, echoes the
# canonical v2 path so the caller's "no blob" message reads sensibly. Read-only.
_resolve_external_blob_read() {
local project="$1" slug="$2" mtype="$3"
local base="$SECRETS_DIR/$project/external/$slug"
case "$mtype" in
file)
echo "$base.file.age" ;;
properties|gradle-properties)
if [ -f "$base.properties.age" ]; then
echo "$base.properties.age"
elif [ -f "$base.gradle-properties.age" ]; then
echo "$base.gradle-properties.age"
else
echo "$base.properties.age"
fi ;;
*)
echo "$base.$mtype.age" ;;
esac
}
# Merge managed key=value lines (from $2) into target file $1, preserving
# all unrelated lines/comments/order. Updates a managed key in place (first
# occurrence), collapses duplicates, appends new keys. Atomic + mode-safe.
@ -710,7 +735,7 @@ pull_external_files() {
continue
fi
local slug; slug=$(_secrets_files_slug "$mpath")
local blob="$SECRETS_DIR/$project/external/$slug.$(_external_blob_suffix "$mtype").age"
local blob; blob=$(_resolve_external_blob_read "$project" "$slug" "$mtype")
if [ ! -f "$blob" ]; then
echo "WARNING: $SECRETS_FILES_NAME names '$mpath' but no encrypted data exists in the store yet. Run 'secrets push' on a machine that has these keys. Skipping." >&2
continue
@ -2053,14 +2078,13 @@ _verify_project() {
while IFS=$'\t' read -r etype epath _; do
[ -n "$etype" ] || continue
slug=$(_secrets_files_slug "$epath")
erel="external/$slug.$(_external_blob_suffix "$etype").age"
# 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.)
# Account for BOTH suffix forms in the orphan set — a dual-written `properties`
# external (additive v2 — EGB-712) legitimately has both blobs on disk; neither
# is an orphan. (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"
eblob=$(_resolve_external_blob_read "$project" "$slug" "$etype")
erel="${eblob#"$pdir"/}"
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
findings=$((findings + 1))

View file

@ -67,6 +67,21 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' >
[ "$status" -eq 0 ]
}
@test "pull reads a v1-suffix properties blob on a v2 store (read-fallback)" {
init_with_remote # born-v2 store (marker=2)
m_gradle_src $'beaconClerkPkTest=pk_test_v1\n'
create_project_dir rffallback
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
"$SECRETS_BIN" push rffallback >/dev/null 2>&1 # writes .properties.age on a v2 store
# Simulate an external that exists only in the v1 suffix (an old client wrote it):
local v2blob; v2blob=$(ls "$SECRETS_DIR/rffallback/external/"*.properties.age)
mv "$v2blob" "${v2blob%.properties.age}.gradle-properties.age"
rm -f "$HOME/.gradle/gradle.properties"
"$SECRETS_BIN" pull rffallback >/dev/null 2>&1
run grep -q 'beaconClerkPkTest=pk_test_v1' "$HOME/.gradle/gradle.properties"
[ "$status" -eq 0 ]
}
# ─── migrate --dry-run / copy-forward (increment 2) ───────────────────
@test "migrate --dry-run reports the rename and writes nothing" {