feat: jq gating, platform-aware install hints, stage-1 docs (EGB-677 stage 1)
- jq required only when .secrets.json exists or is being written; manifest-less projects run jq-free (features skipped with a notice) — clone-and-run for v1 users survives (no-jq fixture excludes /usr/bin, macOS ships jq there now) - check_cmd: platform-aware hints (brew/apt-get/dnf/generic) instead of hardcoded brew — correct guidance on Linux/CI - cmd_help: add command, push flags, manifest section with example - README: manifest section, external files rewritten around .secrets.json (legacy .secrets-files documented as absorbed), troubleshooting entries, command table, test instructions - CLAUDE.md: manifest architecture notes, bash-3.2 '[[ ]] || false' testing convention, project structure refresh
This commit is contained in:
parent
0049584d9b
commit
89e851278b
4 changed files with 168 additions and 22 deletions
77
README.md
77
README.md
|
|
@ -160,7 +160,10 @@ secrets clear
|
|||
|---------|-------------|
|
||||
| `secrets init` | Create the `~/.secrets/` repo and generate an encryption key |
|
||||
| `secrets push` | Encrypt secret files in the current directory and upload them |
|
||||
| `secrets push --frozen` | Sync only what `.secrets.json` declares (skip auto-add) |
|
||||
| `secrets push --dry-run` | Show what would be added/synced without changing anything |
|
||||
| `secrets pull` | Download and decrypt secret files into the current directory |
|
||||
| `secrets add <path>` | Declare a project-relative file in `.secrets.json` |
|
||||
| `secrets clear` | Delete plaintext secret files from the current directory |
|
||||
| `secrets run <command>` | Pull secrets, run a command, then clear secrets when it exits |
|
||||
| `secrets list` | Show all projects that have stored secrets |
|
||||
|
|
@ -176,6 +179,32 @@ When you run `secrets push` or `secrets pull` without specifying a project name,
|
|||
|
||||
You can also specify a name explicitly: `secrets push myapp`.
|
||||
|
||||
### The manifest (`.secrets.json`)
|
||||
|
||||
Every project gets a committed `.secrets.json` at its root declaring exactly what syncs — the single source of truth `push` and `pull` operate from (requires `jq`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"options": { "autoAdd": true },
|
||||
"dotenv": [".env", ".env.staging", "packages/web/.env.development"],
|
||||
"external": [
|
||||
{ "type": "properties", "path": "~/.gradle/gradle.properties",
|
||||
"keys": ["beaconClerkPkTest"] },
|
||||
{ "type": "file", "path": "~/keystores/upload.keystore" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You rarely write it by hand:
|
||||
|
||||
- **Auto-add (default):** `secrets push` discovers conventional files (`.env`, `.env.*`, `.dev.vars` — plus `package.json` workspace dirs once a manifest exists) and adds them to the manifest with an `==>` notice. Commit the manifest so other machines pick it up.
|
||||
- **Explicit mode:** set `"options": {"autoAdd": false}` (a committed, team-shared setting) and `push` only syncs declared entries, warning about undeclared files. `secrets add <path>` is then the only manifest writer. Per-invocation: `push --frozen` (declared-only once) and `push --dry-run` (preview).
|
||||
- `dotenv` paths are project-relative — nested monorepo paths like `packages/@acme/web/.env` are welcome; `..`, absolute paths, and symlinked manifests are refused.
|
||||
- On the other machine, `secrets pull` restores exactly what the committed manifest declares, creating nested directories as needed.
|
||||
|
||||
Projects without a manifest keep working exactly as before (and work without `jq`); the first `push` bootstraps one for you.
|
||||
|
||||
### secrets run
|
||||
|
||||
`secrets run` is a **pull → run → clear** pipeline: it runs `secrets pull` to decrypt the latest files into your project, executes your command, then runs `secrets clear` when that command finishes. Plaintext `.env` / `.dev.vars` files exist only while your command is running.
|
||||
|
|
@ -352,25 +381,40 @@ Requires `jq` (`brew install jq`).
|
|||
|
||||
Some credentials don't live in your project at all. Android builds, for example, read keys from `~/.gradle/gradle.properties` — a global file, outside any project, shared by every Gradle project on your machine (the project's own `gradle.properties` is git-tracked, so it's the wrong home for secrets). `secrets` can sync specific keys from such a file without touching the unrelated keys around them.
|
||||
|
||||
You declare what to sync in a committed `.secrets-files` manifest at your project root, one entry per line:
|
||||
You declare what to sync in the `external` array of your committed `.secrets.json`:
|
||||
|
||||
```
|
||||
# <type> <path> <keys...>
|
||||
gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"external": [
|
||||
{ "type": "properties", "path": "~/.gradle/gradle.properties",
|
||||
"keys": ["beaconClerkPkTest", "beaconClerkPkLive"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **type** — `gradle-properties` (sync named keys) or `file` (sync the whole file — see below).
|
||||
- **path** — absolute or `~/`-relative; must resolve inside `$HOME`. For `gradle-properties` the basename must be `gradle.properties`.
|
||||
- **keys** — the property names to sync (`gradle-properties` only). Only these keys are read on push and merged on pull; everything else in the file is left alone. `file` entries take no keys.
|
||||
- **type** — `properties` (sync named keys from a Java-properties-style file) or `file` (sync the whole file — see below).
|
||||
- **path** — absolute or `~/`-relative; must resolve inside `$HOME`. For `properties` the basename must end in `.properties`.
|
||||
- **keys** — the property names to sync (`properties` only). Only these keys are read on push and merged on pull; everything else in the file is left alone. `file` entries take no keys.
|
||||
|
||||
> **Legacy `.secrets-files`:** older projects declared these entries in a line-based `.secrets-files`. It still parses, and the next `secrets push` absorbs its entries into `.secrets.json` (type `gradle-properties` becomes `properties`) — after that the legacy file is superseded and can be deleted.
|
||||
|
||||
#### Syncing to a second machine
|
||||
|
||||
On the machine that already has the keys set:
|
||||
On the machine that already has the keys set, add the entry to `.secrets.json` (create the file if the project doesn't have one yet):
|
||||
|
||||
```bash
|
||||
cd ~/myapp
|
||||
echo "gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive" > .secrets-files
|
||||
git add .secrets-files && git commit -m "sync gradle Clerk keys"
|
||||
cat > .secrets.json <<'EOF'
|
||||
{
|
||||
"version": 2,
|
||||
"external": [
|
||||
{ "type": "properties", "path": "~/.gradle/gradle.properties",
|
||||
"keys": ["beaconClerkPkTest", "beaconClerkPkLive"] }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
git add .secrets.json && git commit -m "sync gradle Clerk keys"
|
||||
secrets push
|
||||
# ==> Extracted 2 key(s) from ~/.gradle/gradle.properties
|
||||
```
|
||||
|
|
@ -393,9 +437,8 @@ secrets pull
|
|||
|
||||
Some external secrets are whole binary files — an Android upload keystore, a certificate. The `file` type syncs the file verbatim (binary-safe, encrypted with age like everything else):
|
||||
|
||||
```
|
||||
# <type> <path>
|
||||
file ~/keystores/beacon-upload.keystore
|
||||
```json
|
||||
{ "type": "file", "path": "~/keystores/beacon-upload.keystore" }
|
||||
```
|
||||
|
||||
On `secrets push` the file is encrypted into `<project>/external/`. On `secrets pull` it is restored to the same path with mode `600`; if a different version already exists there, it is backed up to `<name>.secrets-bak` first. The same path rules apply (inside `$HOME`, no `..`, no symlinks). Like merged Gradle keys, restored files are permanent plaintext on disk — `secrets clear` does not remove them.
|
||||
|
|
@ -444,10 +487,14 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/`
|
|||
|
||||
**"Fast-forward pull failed"** — Someone else pushed secrets while you had local changes. Run `secrets pull` first, then retry your push.
|
||||
|
||||
**".secrets.json: invalid JSON" / "manifest version N is not supported"** — The committed manifest is malformed or written by a newer `secrets`. The error names the file; fix the syntax, or update the tool (`git pull` in the tool's clone).
|
||||
|
||||
**"'jq' is not installed"** — Manifest features need `jq`. The error prints the install command for your platform. Manifest-less projects work without it.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run the test suite (126 tests)
|
||||
# Run the test suite (174 tests across both files)
|
||||
brew install bats-core
|
||||
bats test/secrets.bats
|
||||
bats test/
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue