v0.2.0.0 feat: sync gradle.properties keys via .secrets-files (EGB-531)

Add a committed .secrets-files manifest that lets secrets track designated
keys from files outside the project root (motivating case:
~/.gradle/gradle.properties for Android Clerk publishable keys, which
Android Studio GUI builds read but terminal env vars can't reach).

- push extracts only the named keys, encrypts under <project>/external/
- pull MERGES them into the target, preserving unrelated keys/comments/order
- pure-bash merge (no sed/regex): exact-string key match, opaque values
- path validator: basename gradle.properties, within $HOME, no symlink/..
- external/ subdir keeps blobs out of the dotenv *.age globs; rekey + list
  recurse explicitly
- which reads back the manifest; list shows [external]; pre-commit blocks
  plaintext gradle.properties

Also fixes two latent bugs in 'secrets rekey' (never completed before, no
prior test): age-keygen refusing to overwrite key.txt, and an EXIT trap
referencing an out-of-scope local under set -u.

Tests: 80 -> 104.

Reviewed via /autoplan (CEO/Eng/DX). EGB-531.
This commit is contained in:
Brian Majewski 2026-05-26 12:42:04 -07:00
parent ac2195d830
commit 110ac514cc
7 changed files with 816 additions and 13 deletions

View file

@ -54,6 +54,8 @@ flowchart TD
Files like `.envrc` (direnv) and `.environment-*` are intentionally **not** tracked.
Beyond project files, `secrets` can also sync designated keys from files that live *outside* the project — like `~/.gradle/gradle.properties` — merging them in without clobbering unrelated keys. See [External files (Gradle properties)](#external-files-gradle-properties).
## Prerequisites
- **macOS** (uses Homebrew for installation)
@ -346,6 +348,47 @@ Inside `~/.secrets/`, workspace secrets are organized by path:
Requires `jq` (`brew install jq`).
### External files (Gradle properties)
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:
```
# <type> <path> <keys...>
gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive
```
- **type**`gradle-properties` (the only supported type today).
- **path** — absolute or `~/`-relative. The basename must be `gradle.properties` and must resolve inside `$HOME`.
- **keys** — the property names to sync. Only these keys are read on push and merged on pull; everything else in the file is left alone.
#### Syncing to a second machine
On the machine that already has the keys set:
```bash
cd ~/myapp
echo "gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive" > .secrets-files
git add .secrets-files && git commit -m "sync gradle Clerk keys"
secrets push
# ==> Extracted 2 key(s) from ~/.gradle/gradle.properties
```
`secrets push` extracts just those keys, encrypts them, and stores them under `<project>/external/` in your secrets repo. Run `secrets which` from the project to confirm the manifest parsed.
On the other machine (after the usual key + repo setup):
```bash
cd ~/myapp
secrets pull
# ==> Merged 2 key(s) into ~/.gradle/gradle.properties (beaconClerkPkTest, beaconClerkPkLive)
```
`secrets pull` merges those keys into the local `~/.gradle/gradle.properties`, leaving every other key untouched. If a managed key already exists, its value is updated in place; comments, ordering, and unrelated entries are preserved. The file is backed up to `gradle.properties.secrets-bak` before the first merge.
> **Note:** unlike `.env` files, merged Gradle keys are written as **permanent plaintext** into the target file — `secrets clear` does **not** remove them. This is appropriate for publishable / low-secrecy values (like Clerk publishable keys, `pk_*`). For high-value secrets that should never sit on disk, use `secrets run` with a `.env` instead.
## Safety features
- **`secrets run` auto-clears** — plaintext files are deleted when the command exits, errors, or is interrupted with Ctrl-C
@ -389,7 +432,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/`
## Development
```bash
# Run the test suite (37 tests)
# Run the test suite (104 tests)
brew install bats-core
bats test/secrets.bats
```