Merge pull request 'v0.6.1.0 feat: secrets migrate guided flow (manifest-free + --status, EGB-710)' (#7) from brian/egb-710-secrets-make-migrate-a-guidedinteractive-flow-not-a-dead-end into main
This commit is contained in:
commit
836b418a12
7 changed files with 612 additions and 39 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -5,6 +5,25 @@ 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.1.0] - 2026-06-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **`secrets migrate` copy-forward is now manifest-free (EGB-710)** — the
|
||||
per-project step enumerates the store's `*.gradle-properties.age` blobs
|
||||
directly (the same source of truth `--finalize` uses) instead of reading
|
||||
`.secrets.json`. A legacy `.secrets-files`-only project now migrates cleanly
|
||||
instead of dead-ending with "No .secrets.json", and a store blob the manifest
|
||||
no longer declares still gets a v2 twin (so `--finalize` won't refuse it).
|
||||
Running migrate in a project with no v1 properties blobs is a clean no-op.
|
||||
|
||||
### Added
|
||||
|
||||
- **`secrets migrate --status`** — a read-only survey that walks every project
|
||||
in the store and reports its v2 readiness (v2-ready / migrated / NEEDS
|
||||
MIGRATE), then whether the store as a whole is finalize-ready. Exits non-zero
|
||||
while any v1 blob is un-twinned, so it can gate the path to `--finalize`.
|
||||
|
||||
## [0.6.0.1] - 2026-06-08
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -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 `<project>/<relpath>.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 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 <path>, 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-<sha>` 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 <path>, version N):`). **Migration is copy-forward and non-destructive:** `secrets migrate --dry-run` (per project, reports old→new, writes nothing) → `secrets migrate` (per project, manifest-free: enumerates the store's `*.gradle-properties.age` blobs directly — same source of truth as `--finalize` — and writes their `.properties.age` twins, so a legacy `.secrets-files`-only project with no `.secrets.json` migrates cleanly and no store blob is left un-twinned; idempotent; EGB-710) → `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-<sha>` 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). `secrets migrate --status` is a read-only survey that walks every project in the store and reports each one's v2 readiness (v2-ready / migrated / NEEDS MIGRATE), exiting non-zero while any v1 blob is un-twinned so it gates the path to `--finalize` (EGB-710). 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/<project>/` 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`
|
||||
|
|
|
|||
|
|
@ -171,7 +171,8 @@ secrets clear
|
|||
| `secrets rekey` | Generate a new encryption key and re-encrypt everything |
|
||||
| `secrets verify [project]` | Check the current project's `.secrets.json` against the store (missing/orphaned blobs) and decrypt every blob. `[project]` overrides the store directory name; the manifest is still read from the current directory |
|
||||
| `secrets verify --all` | Decrypt-test every blob in every project — a store-wide integrity sweep |
|
||||
| `secrets migrate [--dry-run]` | Copy-forward this project's encrypted blobs to store format v2 (non-destructive; `--dry-run` previews) |
|
||||
| `secrets migrate [--dry-run]` | Copy-forward this project's encrypted blobs to store format v2 (non-destructive; manifest-free; `--dry-run` previews) |
|
||||
| `secrets migrate --status` | Survey every project's v2 readiness; exits non-zero until the whole store is finalize-ready |
|
||||
| `secrets migrate --finalize` | Drop the old v1 blobs and mark the store v2 — runs once, store-wide, after `verify` is green and every machine is upgraded |
|
||||
|
||||
### Automatic project detection
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.6.0.1
|
||||
0.6.1.0
|
||||
|
|
|
|||
447
docs/superpowers/plans/2026-06-08-egb-710-migrate-guided-flow.md
Normal file
447
docs/superpowers/plans/2026-06-08-egb-710-migrate-guided-flow.md
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
# EGB-710: `secrets migrate` guided flow (manifest-free copy-forward + `--status` survey) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the dead-end "No .secrets.json — cd into a project that has a manifest" error in `secrets migrate` with a manifest-free copy-forward that just works, plus a `secrets migrate --status` survey that tells the operator exactly which projects still need migrating.
|
||||
|
||||
**Architecture:** The per-project copy-forward step (`_migrate_project`) currently reads `$PWD/.secrets.json` to find `properties` externals, then looks for their v1 blobs. This makes it die on a legacy `.secrets-files`-only project (the real EGB-710 repro) and — worse — silently skips any store blob the manifest doesn't declare, leaving it un-twinned so `--finalize` later refuses it. The fix: enumerate the **store** (`$SECRETS_DIR/<project>/external/*.gradle-properties.age`) directly and copy each to its `.properties.age` twin via suffix-swap — the *exact* logic `_migrate_finalize` already uses. No manifest needed, dotenv/file-only projects become a clean no-op, and migrate twins precisely the blobs finalize will demand twins for. A new `--status` mode walks every project dir and reports per-project readiness + whether the store is finalize-ready.
|
||||
|
||||
**Tech Stack:** Single bash 3.2 script (`secrets`), `age`, `git`, `jq` (unaffected here). Tests: `bats-core` (`test/migrate.bats`). Every standalone `[[ ]]` assertion ends with `|| false` (bash 3.2 ERR-trap gotcha).
|
||||
|
||||
---
|
||||
|
||||
## Background facts (verified against HEAD 633d19e)
|
||||
|
||||
- `_migrate_project()` lives at `secrets:2135`; it `die`s at `secrets:2142-2148` on missing `$PWD/.secrets.json`, then iterates `_json_external_entries "$manifest"` filtered to `gradle-properties` (`secrets:2154-2174`).
|
||||
- `_migrate_finalize()` (`secrets:2196`) already enumerates blobs manifest-free: `find "$SECRETS_DIR" -type f -name '*.gradle-properties.age'` and derives the twin as `new="${f%.gradle-properties.age}.properties.age"` (`secrets:2212-2217`). The per-project step will mirror this, scoped to one project dir.
|
||||
- `cmd_migrate()` flag parser: `secrets:2267-2285`.
|
||||
- `derive_project_name ""` (`secrets:101`) needs no manifest — it uses the git remote basename or `basename "$PWD"`.
|
||||
- `info()`/`die()`: `secrets:36-37`. `ensure_store_protections` is called post-write in the existing copy-forward.
|
||||
- Output-contract strings existing tests depend on (must be preserved): `"would migrate"` (migrate.bats:80), `"0 blob(s) would be copy-forwarded"` (migrate.bats:94), `"1 already present"` (migrate.bats:121, idempotent re-run prints `$already already present`), `"already format v2"` (migrate.bats:139).
|
||||
- The one test that codifies the OLD dead-end — `"migrate with no manifest in cwd dies with a directed message"` (migrate.bats:126) — is the behavior we are intentionally changing; it gets rewritten in Task 1.
|
||||
- No test asserts the non-dry-run "nothing to migrate" wording (grep confirmed), so that message is free to change. We keep the substring `no v1 properties blobs` regardless for safety.
|
||||
- VERSION is `0.6.0.1`; bump to `0.6.1.0` (new subcommand flag + behavior change). `MANIFEST_VERSION` stays `2` (no schema change).
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify: `secrets` — rewrite `_migrate_project` (`secrets:2135-2191`), add `_migrate_status`, extend `cmd_migrate` flag parsing + dispatch (`secrets:2267-2285`), update `cmd_help` migrate lines (`secrets:2308-2309`).
|
||||
- Modify: `test/migrate.bats` — rewrite the dead-end test; add manifest-free, finalize-consistency, and `--status` tests.
|
||||
- Modify: `CLAUDE.md` — update the migrate paragraph (Architecture → Store format) to note manifest-free copy-forward + `--status`.
|
||||
- Modify: `README.md` — migrate usage/help.
|
||||
- Modify: `VERSION` → `0.6.1.0`; `CHANGELOG.md` — new entry.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Manifest-free per-project copy-forward
|
||||
|
||||
**Files:**
|
||||
- Modify: `secrets` — `_migrate_project()` (`secrets:2135-2191`)
|
||||
- Test: `test/migrate.bats`
|
||||
|
||||
- [ ] **Step 1: Write the failing test — migrate works with no `.secrets.json` (the EGB-710 repro)**
|
||||
|
||||
Add to `test/migrate.bats` (after the existing copy-forward tests, ~line 124):
|
||||
|
||||
```bash
|
||||
@test "migrate copy-forwards a v1 properties blob with no .secrets.json (manifest-free)" {
|
||||
# The EGB-710 repro: a legacy project has a v1 properties blob in the store
|
||||
# but no .secrets.json (it predates the manifest). migrate must NOT dead-end.
|
||||
make_v1_store
|
||||
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
create_project_dir nomanifestblob
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
"$SECRETS_BIN" push nomanifestblob >/dev/null 2>&1
|
||||
rm -f .secrets.json # simulate a pre-manifest project
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 0 ]
|
||||
run bash -c "ls $SECRETS_DIR/nomanifestblob/external/*.properties.age"
|
||||
[ "$status" -eq 0 ] # v2 twin written despite no manifest
|
||||
run bash -c "ls $SECRETS_DIR/nomanifestblob/external/*.gradle-properties.age"
|
||||
[ "$status" -eq 0 ] # v1 kept (non-destructive)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to confirm it fails**
|
||||
|
||||
Run: `bats test/migrate.bats -f "manifest-free"`
|
||||
Expected: FAIL — current code `die`s with "No .secrets.json in …" (status 1), so the first `[ "$status" -eq 0 ]` fails.
|
||||
|
||||
- [ ] **Step 3: Rewrite `_migrate_project` to enumerate the store, not the manifest**
|
||||
|
||||
Replace the body of `_migrate_project()` (`secrets:2135-2191`, from `_migrate_project() {` through its closing `}`) with:
|
||||
|
||||
```bash
|
||||
_migrate_project() {
|
||||
local dry_run="$1"
|
||||
if [ "$(_store_format)" = "2" ]; then
|
||||
info "Store is already format v2 — nothing to migrate."
|
||||
return 0
|
||||
fi
|
||||
local project; project=$(derive_project_name "")
|
||||
local pdir="$SECRETS_DIR/$project"
|
||||
|
||||
# Source of truth for the copy-forward is the STORE, not a project manifest.
|
||||
# Every v1 properties blob is a `*.gradle-properties.age` file whose v2 twin
|
||||
# is the same name with the `.properties.age` suffix (the only on-disk change
|
||||
# v2 makes). Enumerating the store — exactly as `_migrate_finalize` does —
|
||||
# means migrate twins precisely the blobs finalize will demand twins for, with
|
||||
# no manifest dependency. This is why a legacy `.secrets-files`-only project
|
||||
# (no `.secrets.json` yet) migrates cleanly instead of dead-ending, and why a
|
||||
# store blob the manifest no longer declares still gets a twin.
|
||||
local moved=0 already=0 would=0 v1count=0 f new
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
v1count=$((v1count + 1))
|
||||
new="${f%.gradle-properties.age}.properties.age"
|
||||
if [ -f "$new" ]; then
|
||||
already=$((already + 1))
|
||||
continue
|
||||
fi
|
||||
if [ "$dry_run" = true ]; then
|
||||
echo "would migrate: ${f#"$SECRETS_DIR"/} -> $(basename "$new")"
|
||||
would=$((would + 1))
|
||||
else
|
||||
cp "$f" "$new"
|
||||
moved=$((moved + 1))
|
||||
fi
|
||||
done < <(find "$pdir/external" -type f -name '*.gradle-properties.age' 2>/dev/null)
|
||||
|
||||
if [ "$dry_run" = true ]; then
|
||||
echo "migrate --dry-run: $would blob(s) would be copy-forwarded for '$project' (writes nothing); $already already present. v1 blobs are kept until 'secrets migrate --finalize'."
|
||||
return 0
|
||||
fi
|
||||
if [ "$moved" -eq 0 ] && [ "$already" -eq 0 ]; then
|
||||
info "Nothing to migrate for '$project' — no v1 properties blobs in the store (already v2-shaped). If you expected one, run 'secrets push' first, then re-run 'secrets migrate'."
|
||||
return 0
|
||||
fi
|
||||
ensure_store_protections
|
||||
git -C "$SECRETS_DIR" add -A
|
||||
git -C "$SECRETS_DIR" commit -m "migrate: copy-forward v2 twins for $project" >/dev/null 2>&1 || true
|
||||
# Push the twins so a --finalize on another machine sees them (finalize
|
||||
# refuses any v1 blob without a twin). Mirrors push/rekey's push behavior.
|
||||
git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1 && git -C "$SECRETS_DIR" push >/dev/null 2>&1 || true
|
||||
info "Copy-forward for '$project': $moved new v2 twin(s), $already already present. v1 blobs kept (non-destructive). Run 'secrets migrate --finalize' once every project is migrated and every machine is upgraded."
|
||||
}
|
||||
```
|
||||
|
||||
Notes for the implementer:
|
||||
- This removes the only in-script caller of `_check_manifest_file`/`_json_external_entries` *inside migrate*; both remain defined and used by push/pull/verify, so do **not** delete them.
|
||||
- `${f#"$SECRETS_DIR"/}` keeps the dry-run line's store-relative form (matches the old `$project/external/...` style closely enough; the test only checks the substring `would migrate`).
|
||||
- The `moved==0 && already==0` branch keeps the substring `no v1 properties blobs`. The idempotent re-run path (`moved==0, already>0`) falls through to the final `info` printing `$already already present`, preserving the `1 already present` contract.
|
||||
|
||||
- [ ] **Step 4: Run the new test + the full migrate suite**
|
||||
|
||||
Run: `bats test/migrate.bats`
|
||||
Expected: the new "manifest-free" test PASSES; existing dry-run/copy-forward/idempotent/finalize tests still PASS; the "migrate with no manifest in cwd dies" test (migrate.bats:126) now FAILS (we fix it in Step 5).
|
||||
|
||||
- [ ] **Step 5: Update the test that codified the old dead-end**
|
||||
|
||||
Replace the test at `test/migrate.bats:126-132` (`"migrate with no manifest in cwd dies with a directed message"`) with:
|
||||
|
||||
```bash
|
||||
@test "migrate in a project with no manifest and no store blobs is a clean no-op" {
|
||||
make_v1_store
|
||||
local dir="$WORK_DIR/nomanifest"; mkdir -p "$dir"; cd "$dir"
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"no v1 properties blobs"* ]] || false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the full suite to confirm green**
|
||||
|
||||
Run: `bats test/migrate.bats`
|
||||
Expected: all PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add secrets test/migrate.bats
|
||||
git commit -m "fix: migrate copy-forward is manifest-free, no dead-end on legacy projects (EGB-710)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Finalize-consistency regression test (store blob not in manifest)
|
||||
|
||||
**Files:**
|
||||
- Test: `test/migrate.bats`
|
||||
|
||||
This proves the latent-bug fix: the old manifest-driven migrate skipped store blobs the manifest didn't declare, leaving them un-twinned so `--finalize` refused them. The rewrite twins them.
|
||||
|
||||
- [ ] **Step 1: Write the test**
|
||||
|
||||
Add to `test/migrate.bats`:
|
||||
|
||||
```bash
|
||||
@test "migrate twins a store blob even when the manifest no longer declares it" {
|
||||
make_v1_store
|
||||
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
create_project_dir staleblob
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
"$SECRETS_BIN" push staleblob >/dev/null 2>&1
|
||||
# The blob is now in the store. Drop the external from the project's manifest
|
||||
# entirely (and remove the legacy file) so NO manifest declares it.
|
||||
printf '{"version":2,"dotenv":[".env",".env.staging"]}\n' > .secrets.json
|
||||
rm -f .secrets-files
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 0 ]
|
||||
run bash -c "ls $SECRETS_DIR/staleblob/external/*.properties.age"
|
||||
[ "$status" -eq 0 ] # twinned despite not being declared anywhere
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it**
|
||||
|
||||
Run: `bats test/migrate.bats -f "no longer declares"`
|
||||
Expected: PASS (the Task 1 rewrite already makes this green — this test guards against regressing back to manifest-driven enumeration).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add test/migrate.bats
|
||||
git commit -m "test: migrate twins undeclared store blobs (finalize-consistency, EGB-710)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `secrets migrate --status` survey
|
||||
|
||||
**Files:**
|
||||
- Modify: `secrets` — add `_migrate_status()`; extend `cmd_migrate` (`secrets:2267-2285`)
|
||||
- Test: `test/migrate.bats`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Add to `test/migrate.bats`:
|
||||
|
||||
```bash
|
||||
@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
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run them to confirm they fail**
|
||||
|
||||
Run: `bats test/migrate.bats -f "status"`
|
||||
Expected: FAIL — `--status` is an unknown flag today (`die "Unknown migrate flag: --status…"`, status 1), so the assertions fail.
|
||||
|
||||
- [ ] **Step 3: Add `_migrate_status` (place it just before `_migrate_finalize`, ~`secrets:2195`)**
|
||||
|
||||
```bash
|
||||
# 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
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wire `--status` into `cmd_migrate`**
|
||||
|
||||
In `cmd_migrate()` (`secrets:2267`), add the `status` local and parse + dispatch. Replace:
|
||||
|
||||
```bash
|
||||
local dry_run=false finalize=false force=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=true; shift ;;
|
||||
--finalize) finalize=true; shift ;;
|
||||
--yes) force=true; shift ;;
|
||||
-*) die "Unknown migrate flag: $1. Usage: secrets migrate [--dry-run | --finalize] [--yes]" ;;
|
||||
*) die "migrate takes no project argument. Run it from inside a project (copy-forward) or use --finalize (store-wide)." ;;
|
||||
esac
|
||||
done
|
||||
if [ "$finalize" = true ]; then
|
||||
_migrate_finalize "$force"
|
||||
else
|
||||
_migrate_project "$dry_run"
|
||||
fi
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```bash
|
||||
local dry_run=false finalize=false force=false status=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=true; shift ;;
|
||||
--finalize) finalize=true; shift ;;
|
||||
--status) status=true; shift ;;
|
||||
--yes) force=true; shift ;;
|
||||
-*) 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 --status / --finalize (store-wide)." ;;
|
||||
esac
|
||||
done
|
||||
if [ "$status" = true ]; then
|
||||
_migrate_status
|
||||
elif [ "$finalize" = true ]; then
|
||||
_migrate_finalize "$force"
|
||||
else
|
||||
_migrate_project "$dry_run"
|
||||
fi
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the status tests + full suite**
|
||||
|
||||
Run: `bats test/migrate.bats`
|
||||
Expected: all PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add secrets test/migrate.bats
|
||||
git commit -m "feat: secrets migrate --status surveys per-project v2 readiness (EGB-710)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Docs, help text, version, changelog
|
||||
|
||||
**Files:**
|
||||
- Modify: `secrets` — `cmd_help` (`secrets:2308-2309`)
|
||||
- Modify: `CLAUDE.md`, `README.md`, `VERSION`, `CHANGELOG.md`
|
||||
|
||||
- [ ] **Step 1: Update `cmd_help` migrate lines**
|
||||
|
||||
In `cmd_help()` replace the two migrate lines (`secrets:2308-2309`):
|
||||
|
||||
```
|
||||
secrets migrate [--dry-run] Copy-forward this project's blobs to store format v2
|
||||
secrets migrate --finalize Drop v1 blobs and mark the store v2 (after verify)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```
|
||||
secrets migrate [--dry-run] Copy-forward this project's v1 blobs to store format v2
|
||||
secrets migrate --status Survey every project's v2 readiness (finalize gate)
|
||||
secrets migrate --finalize Drop v1 blobs and mark the store v2 (after verify)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `CLAUDE.md` migrate paragraph**
|
||||
|
||||
In the "Store format (EGB-677 stage 2 / EGB-703)" bullet, update the migration description: copy-forward is now **manifest-free** — `secrets migrate` enumerates the project's `*.gradle-properties.age` store blobs directly (same source of truth as `--finalize`), so a legacy `.secrets-files`-only project migrates without a `.secrets.json` and no store blob is left un-twinned. Add `secrets migrate --status` to the workflow line as the read-only survey that reports per-project readiness and gates `--finalize`. Reference EGB-710.
|
||||
|
||||
- [ ] **Step 3: Update `README.md`**
|
||||
|
||||
Find the migrate section/help block and add the `--status` line and the manifest-free note, mirroring the help text.
|
||||
|
||||
- [ ] **Step 4: Bump `VERSION`**
|
||||
|
||||
Set `VERSION` to `0.6.1.0`.
|
||||
|
||||
- [ ] **Step 5: Add `CHANGELOG.md` entry**
|
||||
|
||||
Insert above `## [0.6.0.1]`:
|
||||
|
||||
```markdown
|
||||
## [0.6.1.0] - 2026-06-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **`secrets migrate` copy-forward is now manifest-free (EGB-710)** — the
|
||||
per-project step enumerates the store's `*.gradle-properties.age` blobs
|
||||
directly (the same source of truth `--finalize` uses) instead of reading
|
||||
`.secrets.json`. A legacy `.secrets-files`-only project now migrates cleanly
|
||||
instead of dead-ending with "No .secrets.json", and a store blob the manifest
|
||||
no longer declares still gets a v2 twin (so `--finalize` won't refuse it).
|
||||
Running migrate in a project with no v1 properties blobs is a clean no-op.
|
||||
|
||||
### Added
|
||||
|
||||
- **`secrets migrate --status`** — a read-only survey that walks every project
|
||||
in the store and reports its v2 readiness (v2-ready / migrated / NEEDS
|
||||
MIGRATE), then whether the store as a whole is finalize-ready. Exits non-zero
|
||||
while any v1 blob is un-twinned, so it can gate the path to `--finalize`.
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the full bats suite (all files)**
|
||||
|
||||
Run: `bats test/`
|
||||
Expected: all PASS (secrets.bats + manifest.bats + migrate.bats). Confirm the count went up by the tests added here.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add secrets CLAUDE.md README.md VERSION CHANGELOG.md
|
||||
git commit -m "docs: migrate --status + manifest-free copy-forward; bump 0.6.1.0 (EGB-710)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review against the EGB-710 spec
|
||||
|
||||
- **AC: dotenv-only project exits 0 "already v2-shaped"** → Task 1 (`moved==0 && already==0` branch, substring `no v1 properties blobs`). Test: "no manifest and no store blobs is a clean no-op" (Task 1 Step 5).
|
||||
- **AC: `.secrets-files`-only project with v1 blobs migrates instead of the generic error** → the manifest-free rewrite makes it *just migrate* (better than guiding to push first). Test: "manifest-free" (Task 1 Step 1). Note the rewrite supersedes the ticket's "offer to run absorb then migrate" branch — migrate no longer needs the manifest, so there's nothing to prompt for; the only destructive step (`--finalize`) keeps its existing `/dev/tty` confirmation.
|
||||
- **AC: interactive prompts read `/dev/tty`, degrade non-interactively, bash 3.2** → no new prompt is introduced (copy-forward is non-destructive); `--finalize`'s existing `/dev/tty` confirm is untouched. `--status` is pure read-only output. All new code is bash 3.2 (no associative arrays, `[[ ]]` only in tests with `|| false`).
|
||||
- **AC: no behavior change to copy-forward/finalize safety gates** → finalize untouched; copy-forward stays non-destructive (v1 kept, commit + push twins). Verified by the unchanged finalize tests (migrate.bats:168-214).
|
||||
- **AC: bats coverage per branch** → Tasks 1-3 add: manifest-free migrate, no-op no-blobs, undeclared-blob twinning, `--status` needs-migrate / finalize-ready / already-v2.
|
||||
- **Stretch: `--status` survey** → Task 3, delivered.
|
||||
- **Sequencing: independent of the EGB-709 gate** → confirmed; this only touches migrate ergonomics and helps operators *reach* all-v2. EGB-703 safety posture (recovery tag, verify-green gate, twin-before-drop) is preserved.
|
||||
|
||||
## Operator-local follow-up (not part of this plan)
|
||||
|
||||
This repo's `.ship-policy.json` opts out of AI security/red-team passes; before any PR, ask the operator to run `./test/run-security.sh` and complete the SIGNOFF. After landing, the operator can re-run their real-store migration for `onefinalmessage` et al. (`secrets migrate` per project → `secrets migrate --finalize`), which is the path to clearing the EGB-709 v2 gate.
|
||||
103
secrets
103
secrets
|
|
@ -2127,58 +2127,51 @@ cmd_verify() {
|
|||
# secrets migrate --dry-run # per project: report old→new, write nothing
|
||||
# secrets migrate # per project: write v2 twins beside v1 blobs
|
||||
# secrets migrate --finalize # store-wide: verify, drop v1, stamp v2
|
||||
# Per-project (needs the project manifest to know which externals are
|
||||
# `properties`); finalize is store-wide. Mirrors verify's project/--all split.
|
||||
# Per-project copy-forward is manifest-free (enumerates the store's blobs);
|
||||
# finalize is store-wide. Mirrors verify's project/--all split.
|
||||
|
||||
# Copy-forward (or dry-run preview) for the current project. Reads
|
||||
# $PWD/.secrets.json; only `properties` external blobs rename in v2.
|
||||
# Copy-forward (or dry-run preview) for the current project. Manifest-free:
|
||||
# enumerates the store's `*.gradle-properties.age` blobs; only those rename in v2.
|
||||
_migrate_project() {
|
||||
local dry_run="$1"
|
||||
if [ "$(_store_format)" = "2" ]; then
|
||||
info "Store is already format v2 — nothing to migrate."
|
||||
return 0
|
||||
fi
|
||||
local manifest="$PWD/$SECRETS_JSON_NAME"
|
||||
if [ ! -e "$manifest" ]; then
|
||||
die "No $SECRETS_JSON_NAME in $PWD.
|
||||
'secrets migrate' copy-forwards a project's v1 blobs to their v2 names and
|
||||
reads the project manifest to do so. cd into a project that has a manifest,
|
||||
then run 'secrets migrate'. (Store-wide 'secrets migrate --finalize' comes
|
||||
after every project is migrated.)"
|
||||
fi
|
||||
_check_manifest_file "$manifest"
|
||||
local project; project=$(derive_project_name "")
|
||||
local pdir="$SECRETS_DIR/$project"
|
||||
|
||||
local moved=0 already=0 would=0 etype epath slug old new
|
||||
while IFS=$'\t' read -r etype epath _; do
|
||||
[ -n "$etype" ] || continue
|
||||
# Only `properties` blobs change name in v2; dotenv and `file` are already
|
||||
# in their v2 shape and never move.
|
||||
[ "$etype" = "gradle-properties" ] || continue
|
||||
slug=$(_secrets_files_slug "$epath")
|
||||
old="$pdir/external/$slug.gradle-properties.age"
|
||||
new="$pdir/external/$slug.properties.age"
|
||||
[ -f "$old" ] || continue # nothing pushed yet (or already dropped)
|
||||
if [ -f "$new" ]; then # idempotent: twin already exists
|
||||
# Source of truth for the copy-forward is the STORE, not a project manifest.
|
||||
# Every v1 properties blob is a `*.gradle-properties.age` file whose v2 twin
|
||||
# is the same name with the `.properties.age` suffix (the only on-disk change
|
||||
# v2 makes). Enumerating the store — exactly as `_migrate_finalize` does —
|
||||
# means migrate twins precisely the blobs finalize will demand twins for, with
|
||||
# no manifest dependency. This is why a legacy `.secrets-files`-only project
|
||||
# (no `.secrets.json` yet) migrates cleanly instead of dead-ending, and why a
|
||||
# store blob the manifest no longer declares still gets a twin.
|
||||
local moved=0 already=0 would=0 f new
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
new="${f%.gradle-properties.age}.properties.age"
|
||||
if [ -f "$new" ]; then
|
||||
already=$((already + 1))
|
||||
continue
|
||||
fi
|
||||
if [ "$dry_run" = true ]; then
|
||||
echo "would migrate: $project/external/$slug.gradle-properties.age -> $slug.properties.age"
|
||||
echo "would migrate: ${f#"$SECRETS_DIR"/} -> $(basename "$new")"
|
||||
would=$((would + 1))
|
||||
else
|
||||
cp "$old" "$new"
|
||||
cp "$f" "$new"
|
||||
moved=$((moved + 1))
|
||||
fi
|
||||
done < <(_json_external_entries "$manifest")
|
||||
done < <(find "$pdir/external" -type f -name '*.gradle-properties.age' 2>/dev/null)
|
||||
|
||||
if [ "$dry_run" = true ]; then
|
||||
echo "migrate --dry-run: $would blob(s) would be copy-forwarded for '$project' (writes nothing); $already already present. v1 blobs are kept until 'secrets migrate --finalize'."
|
||||
return 0
|
||||
fi
|
||||
if [ "$moved" -eq 0 ] && [ "$already" -eq 0 ]; then
|
||||
info "Nothing to migrate for '$project' (no v1 properties blobs)."
|
||||
info "Nothing to migrate for '$project' — no v1 properties blobs in the store (already v2-shaped). If you expected one, run 'secrets push' first, then re-run 'secrets migrate'."
|
||||
return 0
|
||||
fi
|
||||
ensure_store_protections
|
||||
|
|
@ -2190,6 +2183,46 @@ _migrate_project() {
|
|||
info "Copy-forward for '$project': $moved new v2 twin(s), $already already present. v1 blobs kept (non-destructive). Run 'secrets migrate --finalize' once every project is migrated and every machine is upgraded."
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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,
|
||||
# stamps the marker, then drops v1 blobs.
|
||||
|
|
@ -2267,17 +2300,20 @@ $untwinned cd into each project and run 'secrets migrate', then re-run 'secrets
|
|||
cmd_migrate() {
|
||||
resolve_store
|
||||
check_initialized
|
||||
local dry_run=false finalize=false force=false
|
||||
local dry_run=false finalize=false force=false status=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=true; shift ;;
|
||||
--finalize) finalize=true; shift ;;
|
||||
--status) status=true; shift ;;
|
||||
--yes) force=true; shift ;;
|
||||
-*) die "Unknown migrate flag: $1. Usage: secrets migrate [--dry-run | --finalize] [--yes]" ;;
|
||||
*) die "migrate takes no project argument. Run it from inside a project (copy-forward) or use --finalize (store-wide)." ;;
|
||||
-*) 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 --status / --finalize (store-wide)." ;;
|
||||
esac
|
||||
done
|
||||
if [ "$finalize" = true ]; then
|
||||
if [ "$status" = true ]; then
|
||||
_migrate_status
|
||||
elif [ "$finalize" = true ]; then
|
||||
_migrate_finalize "$force"
|
||||
else
|
||||
_migrate_project "$dry_run"
|
||||
|
|
@ -2305,7 +2341,8 @@ Usage:
|
|||
secrets rekey Re-encrypt all secrets with a new key
|
||||
secrets verify [project] Check the manifest against the store + decrypt every blob
|
||||
secrets verify --all Decrypt-test every blob in every project (integrity gate)
|
||||
secrets migrate [--dry-run] Copy-forward this project's blobs to store format v2
|
||||
secrets migrate [--dry-run] Copy-forward this project's v1 blobs to store format v2
|
||||
secrets migrate --status Survey every project's v2 readiness (finalize gate)
|
||||
secrets migrate --finalize Drop v1 blobs and mark the store v2 (after verify)
|
||||
secrets which Show the active store, manifest, and external entries
|
||||
secrets where Alias for `which`
|
||||
|
|
|
|||
|
|
@ -123,12 +123,47 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' >
|
|||
[ "$output" = "1" ]
|
||||
}
|
||||
|
||||
@test "migrate with no manifest in cwd dies with a directed message" {
|
||||
@test "migrate copy-forwards a v1 properties blob with no .secrets.json (manifest-free)" {
|
||||
# The EGB-710 repro: a legacy project has a v1 properties blob in the store
|
||||
# but no .secrets.json (it predates the manifest). migrate must NOT dead-end.
|
||||
make_v1_store
|
||||
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
create_project_dir nomanifestblob
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
"$SECRETS_BIN" push nomanifestblob >/dev/null 2>&1
|
||||
rm -f .secrets.json # simulate a pre-manifest project
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 0 ]
|
||||
run bash -c "ls $SECRETS_DIR/nomanifestblob/external/*.properties.age"
|
||||
[ "$status" -eq 0 ] # v2 twin written despite no manifest
|
||||
run bash -c "ls $SECRETS_DIR/nomanifestblob/external/*.gradle-properties.age"
|
||||
[ "$status" -eq 0 ] # v1 kept (non-destructive)
|
||||
}
|
||||
|
||||
@test "migrate twins a store blob even when the manifest no longer declares it" {
|
||||
make_v1_store
|
||||
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
create_project_dir staleblob
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
"$SECRETS_BIN" push staleblob >/dev/null 2>&1
|
||||
# The blob is now in the store. Drop the external from the project's manifest
|
||||
# entirely (and remove the legacy file) so NO manifest declares it.
|
||||
printf '{"version":2,"dotenv":[".env",".env.staging"]}\n' > .secrets.json
|
||||
rm -f .secrets-files
|
||||
run bash -c "ls $SECRETS_DIR/staleblob/external/*.gradle-properties.age"
|
||||
[ "$status" -eq 0 ] # precondition: the v1 blob exists in the store
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 0 ]
|
||||
run bash -c "ls $SECRETS_DIR/staleblob/external/*.properties.age"
|
||||
[ "$status" -eq 0 ] # twinned despite not being declared anywhere
|
||||
}
|
||||
|
||||
@test "migrate in a project with no manifest and no store blobs is a clean no-op" {
|
||||
make_v1_store
|
||||
local dir="$WORK_DIR/nomanifest"; mkdir -p "$dir"; cd "$dir"
|
||||
run "$SECRETS_BIN" migrate
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *".secrets.json"* ]] || false
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"no v1 properties blobs"* ]] || false
|
||||
}
|
||||
|
||||
@test "migrate on an already-v2 store is a no-op" {
|
||||
|
|
@ -311,6 +346,40 @@ m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' >
|
|||
[[ "$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)" {
|
||||
make_v1_store
|
||||
create_project_dir garbagemarker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue