Merge origin/main into EGB-283 (multi-recipient age encryption)
Reconcile the multi-recipient branch (cut from v0.6.1.0) with origin/main
at v0.7.4.0. The two feature lines are largely orthogonal; the one real
integration point is the external-blob encrypt path:
- EGB-712 added an additive-v2 dual-write loop (_external_blob_write_targets,
writing v2 + any v1 twin). EGB-283 routes every encrypt site through
RECIPIENT_ARGS for N-recipient encryption. Resolution keeps the dual-write
loop but encrypts each target to the full recipient set
(age "${RECIPIENT_ARGS[@]}" per write target), so dual-write and
multi-recipient compose. cmd_push loads recipients before both external
push sites; legacy single-key rekey keeps its fresh-keypair pubkey path.
Version: 0.6.2.0 + 0.7.4.0 -> 0.7.5.0. Docs (CLAUDE.md/README/CHANGELOG)
merged to carry both feature sets; subcommand list now includes
recipients/reencrypt and upgrade.
Tests: full `bats test/` green except 6 pre-existing host-environment
failures (4 chmod-600 restore assertions + 2 jq-PATH-shadow tests, all
macOS-authored), none touching merged code. recipients.bats 34/34 pass;
external/dual-write area passes except the same mode-600 host artifacts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
e29024bd63
16 changed files with 2723 additions and 142 deletions
135
CHANGELOG.md
135
CHANGELOG.md
|
|
@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to a four-digit MAJOR.MINOR.PATCH.MICRO version scheme.
|
||||
|
||||
## [0.6.2.0] - 2026-06-24
|
||||
## [0.7.5.0] - 2026-06-24
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -36,6 +36,139 @@ and this project adheres to a four-digit MAJOR.MINOR.PATCH.MICRO version scheme.
|
|||
(skipped on legacy stores). Exits non-zero on any count mismatch so it can
|
||||
gate CI or a migration.
|
||||
|
||||
## [0.7.4.0] - 2026-06-18
|
||||
|
||||
### Added
|
||||
|
||||
- **`secrets upgrade` verb (EGB-716)** — the fix path paired with the EGB-713
|
||||
version-skew *warning*. Until now the warning told you you were behind but not
|
||||
how to catch up; `secrets upgrade` closes that loop.
|
||||
- **`secrets upgrade`** — `git -C "$SCRIPT_DIR" pull --ff-only` on the tool's
|
||||
own checkout (fast-forward only — never merges or rewrites local commits),
|
||||
reports `vOLD -> vNEW`, then best-effort re-checks the store's recorded
|
||||
writer-version against the new version so you see whether the EGB-713 nudge
|
||||
is now cleared (the new code itself takes effect on your next command).
|
||||
- **`secrets upgrade --check`** — reports whether an update is available
|
||||
(`git fetch` + compare to upstream) and changes nothing.
|
||||
- Deliberately thin: no auto-update, no background polling (this is a security
|
||||
tool). Directed errors for not-a-git-checkout, no upstream, a diverged/dirty
|
||||
branch, or being offline.
|
||||
|
||||
## [0.7.3.1] - 2026-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
- **EGB-677 stage-1 structural cleanups (EGB-701)** — tech-debt dedup with one
|
||||
new safety warning; no behavior change for the manifest-driven (v2) happy path.
|
||||
- **`secrets which` now reuses the one external-entry extractor** the push/pull
|
||||
path uses (`_json_external_entries`) instead of its own duplicated `jq`
|
||||
projection. So `which` applies the same `properties`→`gradle-properties`
|
||||
normalization and skips (with a warning) the same malformed external entries
|
||||
the sync path drops — `which` shows exactly what will sync, not a stale raw
|
||||
projection that could drift from the real behavior.
|
||||
- **The two external-manifest read guards are factored into shared helpers** —
|
||||
`_json_readable` (plain regular file, silent) and `_legacy_readable` (warns
|
||||
and skips a symlinked `.secrets-files`) — so `_external_entries_for_push` and
|
||||
`_external_entries_for_pull` can't drift apart.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Legacy (manifest-less) `pull` no longer silently under-restores (EGB-701)** —
|
||||
the manifest-less pull path globs only top-level `*.age`/`.*.age`, so a nested
|
||||
dotenv blob (`<project>/<relpath>.age`) written by a manifest-driven push on
|
||||
another machine was invisible: restored nothing, counted nothing, said nothing.
|
||||
It now **warns** and names each nested blob it can't reach (external blobs are
|
||||
excluded — `pull_external_files` handles those), pointing at committing a
|
||||
`.secrets.json` as the fix. The manifest-driven pull already restored nesting
|
||||
correctly; this only closes the legacy path's blind spot.
|
||||
|
||||
## [0.7.3.0] - 2026-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Real install / onboarding scripts (EGB-671)** — onboarding a machine is now
|
||||
(close to) one command, and a mis-copied key fails loudly instead of silently.
|
||||
- **`secrets join --remote <url> --key <path>`** — second-machine onboarding in
|
||||
one verb: clones the vault, installs the key at mode 600, and **verifies the
|
||||
key actually decrypts the store before declaring success**. An empty vault
|
||||
reports "nothing to verify yet" (it never prints a false `VERIFIED`); a wrong
|
||||
key fails loudly with the store left in place to fix. All security logic
|
||||
(store resolution, URL handling, path rails) is reused from the audited core,
|
||||
not re-implemented in a side script.
|
||||
- **`secrets init --remote <url>`** — wires the remote and pushes the initial
|
||||
store so the upstream branch exists, so your first project `push` doesn't trip
|
||||
the fast-forward-pull guard on a brand-new empty remote. Run interactively,
|
||||
`init` also offers to add your first project's secrets (default No, skipped
|
||||
under `--yes` / non-interactive, so it stays a clean primitive for CI).
|
||||
- **`install.sh`** — thin bootstrap that ships in the repo: checks `age` + `jq`
|
||||
+ `git`, then prints the `PATH` line, the onboarding next-steps, the upgrade
|
||||
one-liner, and a key-transfer hint. It never edits your shell config and never
|
||||
runs `sudo` (it prints the command so you stay in control).
|
||||
- **First-manifest `options.autoAdd` prompt (EGB-677 contract #2)** — the first
|
||||
`push` that scaffolds a project's manifest now records an explicit, committed
|
||||
`options.autoAdd` value (asked once when interactive; the default ON, written
|
||||
explicitly, under automation).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Day-2 silent decrypt failure** — `secrets pull` now dies loudly when a blob
|
||||
fails to decrypt with the current key (all three decrypt paths), instead of
|
||||
emitting a warning and continuing with exit 0. A wrong key can no longer pass
|
||||
unnoticed after onboarding.
|
||||
- The `secrets init` second-machine trap now points at `secrets join` (the real
|
||||
one-command path) instead of a manual `git clone`.
|
||||
|
||||
## [0.7.2.0] - 2026-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- **`secrets list --json` (EGB-699)** — machine-readable listing for tooling and
|
||||
CI. Emits a single JSON object on stdout: `{"store", "projects": [{"name",
|
||||
"entries": [...]}]}`, where each entry self-describes via a `type`
|
||||
discriminator — `{"type":"dotenv","path":<relpath>}` or
|
||||
`{"type":"external","subtype":"properties"|"file","path":<slug>}`. Reflects the
|
||||
same recursive store walk as the human `list` (nested `<project>/<relpath>.age`
|
||||
+ `external/<slug>.age`). jq does the assembly so paths escape correctly; the
|
||||
human store hint is suppressed so stdout stays pure JSON (notices → stderr).
|
||||
jq is required only for `--json`. Feeds the EGB-671 install scripts, which need
|
||||
to enumerate a cloned store programmatically instead of scraping the table.
|
||||
|
||||
## [0.7.1.0] - 2026-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Version-skew nudge (EGB-713)** — the store now records the highest `secrets`
|
||||
version that has written to it (`.secrets-writer-version`, committed,
|
||||
monotonic). When you run a command against a store last written by a *newer*
|
||||
`secrets` than your own, you get a one-line non-fatal stderr nudge to update
|
||||
your tool; `secrets which` shows the store's `written-by:` version (and flags
|
||||
when you're behind). Stores written by older builds carry no stamp and stay
|
||||
silent — no false alarms. The loud counterpart to EGB-712's quiet
|
||||
forcing function.
|
||||
|
||||
## [0.7.0.0] - 2026-06-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **Additive store-format v2 (EGB-712)** — upgraded `secrets` clients now read
|
||||
either external blob suffix (`.properties.age` or the legacy
|
||||
`.gradle-properties.age`) and **dual-write** a `properties` external whenever a
|
||||
v1 twin already exists in the store. Existing externals keep working for
|
||||
teammates on an older `secrets`; only a brand-new `properties` external is
|
||||
written v2-only (a gentle "upgrade to see it" forcing function). dotenv and
|
||||
whole-`file` externals are unchanged across formats and always propagate.
|
||||
- **`secrets migrate --finalize` is now optional GC**, not a required milestone.
|
||||
Because clients dual-write and read-fall-back, no teammate is ever cut off by
|
||||
*not* finalizing; finalize only reclaims the duplicate v1 blobs, and stays
|
||||
deferrable indefinitely. Its safety gates are unchanged. This defuses the
|
||||
cross-machine "all clients must be v2 before finalize" coordination gate.
|
||||
|
||||
### Added
|
||||
|
||||
- **`secrets migrate --status`** now reports `v2-only` externals per project
|
||||
(the ones an un-upgraded client cannot read), so you can see the forcing
|
||||
function's footprint at a glance.
|
||||
|
||||
## [0.6.1.0] - 2026-06-08
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue