From 94ee6ec9a28410c269d8a08c0fcb7fa85851930e Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Mon, 8 Jun 2026 06:02:11 -0700 Subject: [PATCH 1/3] feat: secrets which prints the manifest version (EGB-700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes EGB-700 — the store-format line (format: vN) shipped in v0.6.0.0; this adds the manifest schema version to the `which` manifest header (version N, validated == MANIFEST_VERSION by _check_manifest_file). Now a single `secrets which` surfaces both the store format and the manifest version for the dual-format debugging window. 1 bats test. Suite 237/237. --- secrets | 5 ++++- test/manifest.bats | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/secrets b/secrets index 97676b8..6b18cfe 100755 --- a/secrets +++ b/secrets @@ -1918,7 +1918,10 @@ cmd_which() { local json_manifest="$PWD/$SECRETS_JSON_NAME" if [ -e "$json_manifest" ]; then _check_manifest_file "$json_manifest" - echo "manifest ($SECRETS_JSON_NAME at $json_manifest):" + # EGB-700: surface the manifest schema version (validated == MANIFEST_VERSION + # by _check_manifest_file above) alongside the store format printed earlier. + local mver; mver=$(jq -r '.version // "?"' "$json_manifest") + echo "manifest ($SECRETS_JSON_NAME at $json_manifest, version $mver):" local entry while IFS= read -r entry; do [ -n "$entry" ] || continue diff --git a/test/manifest.bats b/test/manifest.bats index 37564c7..eb5c42a 100644 --- a/test/manifest.bats +++ b/test/manifest.bats @@ -97,6 +97,14 @@ load test_helper [[ "$output" == *".env"* ]] || false } +@test "which prints the manifest version (EGB-700)" { + create_project_dir manifestver + "$SECRETS_BIN" add .env >/dev/null + run "$SECRETS_BIN" which + [ "$status" -eq 0 ] + [[ "$output" == *"version 2"* ]] || false +} + @test "malformed .secrets.json dies with a directed error naming the file" { create_project_dir addproj echo '{ not json' > .secrets.json From 6a84846e6451e61653b503305529ae080aa7be07 Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Mon, 8 Jun 2026 06:19:57 -0700 Subject: [PATCH 2/3] chore: bump version and changelog (v0.6.0.1) Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 9 +++++++++ VERSION | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5167f..026df25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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.0.1] - 2026-06-08 + +### Added + +- **`secrets which` now prints the manifest version (EGB-700)** — the manifest + header line shows `version N` alongside the store format, so a single + `secrets which` tells you both the on-disk store format and the `.secrets.json` + schema version at a glance. + ## [0.6.0.0] - 2026-06-07 ### Added diff --git a/VERSION b/VERSION index fdae70d..758efdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0.0 +0.6.0.1 From 2549832f0e06e45c04429054c095aaaf325adee4 Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Mon, 8 Jun 2026 06:21:26 -0700 Subject: [PATCH 3/3] docs: sync test counts and which manifest-version note (EGB-700) - README: test suite total 236 -> 237 - CLAUDE.md: manifest.bats 77 -> 78 tests - CLAUDE.md: note `secrets which` now prints the .secrets.json schema version in its manifest header line alongside `format: vN` Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2263b20..ece6714 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek - Storage: Private git repo at `~/.secrets/` - Convention: Tracks `.env`, `.env.*`, and `.dev.vars` (not `.envrc`, `.environment-*`) - Manifest (EGB-677 stage 1): committed `.secrets.json` is the source of truth for what syncs — `dotenv[]` (project-relative, nested ok, `@` allowed; rail rejects `..`/absolute/symlink) + `external[]` (`properties`/`file`). Push discovery auto-adds (gated by committed `options.autoAdd`, default ON; `--frozen`/`--dry-run` overrides), bootstraps the manifest on first push (written only after ≥1 blob encrypts), and absorbs a legacy `.secrets-files` (gradle-properties → `properties`; on pull the legacy file is superseded with a warning). Store layout: nested dotenv entries land at `/.age` (relpath preserved — the store self-describes where a file restores). jq is a hard dep only when a manifest exists/is written; manifest-less projects run jq-free (manifest features skipped with a notice). `check_cmd` prints platform-aware install hints. -- Store format (EGB-677 stage 2 / EGB-703): the store is self-describing via a committed one-line `$SECRETS_DIR/.secrets-format` file (`2`). Absence ⇒ v1 (every store predating EGB-703). v2's only on-disk change vs v1 is the external `properties` blob suffix: `.gradle-properties.age` → `.properties.age` (matching the manifest `type`); dotenv and `file` blobs are unchanged. `_store_format()` reads the marker; `_external_blob_suffix(type)` is the single source of truth for the suffix (push/pull/verify all route through it, so v1 and v2 stores never disagree on where a blob lives). `init` stamps a fresh store v2 (born-v2). `secrets which` prints `format: vN`. **Migration is copy-forward and non-destructive:** `secrets migrate --dry-run` (per project, reports old→new, writes nothing) → `secrets migrate` (per project, writes `.properties.age` twins beside the v1 blobs; needs the project manifest to know which externals are `properties`; idempotent) → `secrets migrate --finalize` (store-wide; the ONLY destructive step — gates on `verify --all` green + every v1 blob having a v2 twin, cuts a `pre-v2-migrate-` recovery tag, stamps the marker, then drops v1 blobs; refuses without `--yes`/operator confirmation since a lagging v1 client against a finalized store stops seeing `properties` externals until it upgrades). The deliberate flatten-to-basename naming the EGB-677 CEO plan sketched was dropped as lossy (it discards the restore relpath that makes the store self-describing) — see the EGB-703 eureka. +- Store format (EGB-677 stage 2 / EGB-703): the store is self-describing via a committed one-line `$SECRETS_DIR/.secrets-format` file (`2`). Absence ⇒ v1 (every store predating EGB-703). v2's only on-disk change vs v1 is the external `properties` blob suffix: `.gradle-properties.age` → `.properties.age` (matching the manifest `type`); dotenv and `file` blobs are unchanged. `_store_format()` reads the marker; `_external_blob_suffix(type)` is the single source of truth for the suffix (push/pull/verify all route through it, so v1 and v2 stores never disagree on where a blob lives). `init` stamps a fresh store v2 (born-v2). `secrets which` prints the store-format line `format: vN`, and (EGB-700) when a `.secrets.json` is present the manifest header line also carries its schema version (`manifest (.secrets.json at , version N):`). **Migration is copy-forward and non-destructive:** `secrets migrate --dry-run` (per project, reports old→new, writes nothing) → `secrets migrate` (per project, writes `.properties.age` twins beside the v1 blobs; needs the project manifest to know which externals are `properties`; idempotent) → `secrets migrate --finalize` (store-wide; the ONLY destructive step — gates on `verify --all` green + every v1 blob having a v2 twin, cuts a `pre-v2-migrate-` recovery tag, stamps the marker, then drops v1 blobs; refuses without `--yes`/operator confirmation since a lagging v1 client against a finalized store stops seeing `properties` externals until it upgrades). The deliberate flatten-to-basename naming the EGB-677 CEO plan sketched was dropped as lossy (it discards the restore relpath that makes the store self-describing) — see the EGB-703 eureka. - Verify (EGB-698): `secrets verify` is a read-only integrity check. Default mode (current project) cross-checks `$PWD/.secrets.json` against `$SECRETS_DIR//` both ways (declared-but-missing blobs + orphaned blobs) and decrypt-tests every blob (dotenv + external) by streaming plaintext to `/dev/null` (never written to disk). `secrets verify --all` decrypt-tests every blob in every project (integrity only — the store carries no manifests, so consistency can't be checked store-wide). Both recurse the whole project tree (`find -type f`, same as rekey/list). Exits non-zero on any finding so it can gate the stage-2 `migrate --finalize` and CI. The store deliberately holds no manifest — `.secrets.json` is committed in each project's own repo and read from `$PWD`. - External files: `.secrets-files` manifest tracks designated keys from files outside the project (e.g. `~/.gradle/gradle.properties`, merged not overwritten — EGB-531) and whole binary files (type `file`, e.g. an Android upload keystore — EGB-652); see below - Workspaces: `--workspaces` flag reads `package.json` workspaces, requires `jq` @@ -76,7 +76,7 @@ secrets # CLI script (~2000 lines bash) hooks/pre-commit # Pre-commit hook template test/ secrets.bats # bats-core test suite (133 tests) - manifest.bats # EGB-677 .secrets.json manifest tests (77 tests) + manifest.bats # EGB-677 .secrets.json manifest tests (78 tests) migrate.bats # EGB-703 store-format-v2 migration tests (26 tests) test_helper.bash # Shared setup/teardown README.md # User-facing documentation diff --git a/README.md b/README.md index 10cf7dc..50fb49f 100644 --- a/README.md +++ b/README.md @@ -500,7 +500,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/` ## Development ```bash -# Run the test suite (236 tests across three files) +# Run the test suite (237 tests across three files) brew install bats-core bats test/