feat: twin-rule write targets — dual-write existing, v2-only for new (additive v2, EGB-712)

This commit is contained in:
Brian Majewski 2026-06-08 10:31:00 -07:00
parent 2866e5f4b1
commit 2f36fe1898
2 changed files with 73 additions and 19 deletions

47
secrets
View file

@ -539,21 +539,6 @@ _store_format() {
echo 1
}
# The on-disk blob suffix for an external entry, format-aware. v2 unifies
# the legacy `gradle-properties` suffix to `properties` (matching the JSON
# manifest `type`); `file` is unchanged in both formats. The slug + this
# suffix + `.age` is the external blob name. This is the single source of
# truth for the suffix — push, pull, verify all route through it so a v1
# and a v2 store can never disagree on where a blob lives.
_external_blob_suffix() {
local mtype="$1"
if [ "$mtype" = "gradle-properties" ] && [ "$(_store_format)" = "2" ]; then
echo "properties"
else
echo "$mtype"
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
@ -579,6 +564,26 @@ _resolve_external_blob_read() {
esac
}
# The on-disk path(s) to WRITE for an external blob, one per line. For a
# `properties` external this is the v2 suffix (.properties.age) ALWAYS, plus the
# v1 suffix (.gradle-properties.age) WHEN a v1 twin already exists in the store
# (dual-write keeps old clients fresh; a brand-new external is v2-only — the
# intended forcing function, additive v2 / EGB-712). `file` externals have a
# single suffix in both formats. Independent of the store marker.
_external_blob_write_targets() {
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)
echo "$base.properties.age"
[ -f "$base.gradle-properties.age" ] && echo "$base.gradle-properties.age" ;;
*)
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.
@ -677,7 +682,11 @@ push_external_files() {
# EGB-652: whole-file sync — encrypt the file verbatim (binary-safe).
mkdir -p "$SECRETS_DIR/$project/external"
local fslug; fslug=$(_secrets_files_slug "$mpath")
age -r "$pubkey" -o "$SECRETS_DIR/$project/external/$fslug.$(_external_blob_suffix file).age" "$expanded"
local wt
while IFS= read -r wt; do
[ -n "$wt" ] || continue
age -r "$pubkey" -o "$wt" "$expanded"
done < <(_external_blob_write_targets "$project" "$fslug" file)
info "Encrypted file $mpath"
pushed=$((pushed + 1))
continue
@ -706,7 +715,11 @@ push_external_files() {
fi
mkdir -p "$SECRETS_DIR/$project/external"
local slug; slug=$(_secrets_files_slug "$mpath")
age -r "$pubkey" -o "$SECRETS_DIR/$project/external/$slug.$(_external_blob_suffix "$mtype").age" "$tmp"
local wt
while IFS= read -r wt; do
[ -n "$wt" ] || continue
age -r "$pubkey" -o "$wt" "$tmp"
done < <(_external_blob_write_targets "$project" "$slug" "$mtype")
rm -f "$tmp"
info "Extracted $found key(s) from $mpath"
pushed=$((pushed + 1))