Add support for package.json workspaces in secrets CLI
- Introduced `--workspaces` flag for `push` and `pull` commands to handle environment files in monorepos. - Updated README and CLAUDE.md to reflect new workspace functionality and installation instructions. - Enhanced test suite with cases for workspace operations, ensuring proper encryption and decryption of environment files. - Improved error handling for missing package.json and workspaces field. - Increased test coverage from 20 to 25 tests.
This commit is contained in:
parent
7eae4ea9a1
commit
585367b9a6
4 changed files with 392 additions and 63 deletions
|
|
@ -26,6 +26,7 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek
|
||||||
- Encryption: `age` with key files (not passphrases — age passphrases are non-scriptable)
|
- Encryption: `age` with key files (not passphrases — age passphrases are non-scriptable)
|
||||||
- Storage: Private git repo at `~/.secrets/`
|
- Storage: Private git repo at `~/.secrets/`
|
||||||
- Convention: Globs `.env` and `.env.*` (not `.envrc`, `.environment-*`)
|
- Convention: Globs `.env` and `.env.*` (not `.envrc`, `.environment-*`)
|
||||||
|
- Workspaces: `--workspaces` flag reads `package.json` workspaces, requires `jq`
|
||||||
- Safety: Pre-commit hook rejects plaintext `.env` files
|
- Safety: Pre-commit hook rejects plaintext `.env` files
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
@ -34,7 +35,7 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek
|
||||||
secrets # CLI script (~300 lines bash)
|
secrets # CLI script (~300 lines bash)
|
||||||
hooks/pre-commit # Pre-commit hook template
|
hooks/pre-commit # Pre-commit hook template
|
||||||
test/
|
test/
|
||||||
secrets.bats # bats-core test suite (20 tests)
|
secrets.bats # bats-core test suite (25 tests)
|
||||||
test_helper.bash # Shared setup/teardown
|
test_helper.bash # Shared setup/teardown
|
||||||
README.md # User-facing documentation
|
README.md # User-facing documentation
|
||||||
CLAUDE.md # This file
|
CLAUDE.md # This file
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -5,23 +5,68 @@ Sync `.env` files between machines without storing them in git. Encrypts with [a
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 1. Install age (encryption tool)
|
||||||
brew install age
|
brew install age
|
||||||
# Clone this repo or copy the `secrets` script to your PATH
|
|
||||||
|
# 2. Clone this repo (the tool's source code)
|
||||||
|
git clone git@github.com:<you>/secrets.git ~/dev/secrets
|
||||||
|
|
||||||
|
# 3. Add it to your PATH (e.g., in ~/.zshrc)
|
||||||
|
export PATH="$HOME/dev/secrets:$PATH"
|
||||||
|
|
||||||
|
# 4. Initialize the encrypted secrets store (separate repo)
|
||||||
|
secrets init
|
||||||
|
|
||||||
|
# 5. Create a PRIVATE repo on GitHub for your encrypted secrets, then:
|
||||||
|
cd ~/.secrets
|
||||||
|
git remote add origin git@github.com:<you>/my-secrets.git
|
||||||
|
git push -u origin main
|
||||||
|
|
||||||
|
# 6. Copy the key file to your other machine (one-time)
|
||||||
|
scp ~/.secrets/key.txt <other-machine>:~/.secrets/key.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This repo (`~/dev/secrets`) is the **tool** — the CLI script, tests, and docs.
|
||||||
|
`~/.secrets/` is the **encrypted secrets store** — a separate private git repo
|
||||||
|
where your `.env.age` files live. They are two different repos.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
secrets init # Create ~/.secrets repo + generate age key
|
secrets init # Create ~/.secrets repo + generate age key
|
||||||
secrets push [project] # Encrypt .env* files and push
|
secrets push [project] # Encrypt .env* files and push
|
||||||
secrets pull [project] # Pull and decrypt .env* files into current dir
|
secrets pull [project] # Pull and decrypt .env* files into current dir
|
||||||
secrets list # Show all projects
|
secrets push -w|--workspaces # Push .env* from all package.json workspaces
|
||||||
secrets rm <project> # Remove a project's secrets
|
secrets pull -w|--workspaces # Pull .env* into all package.json workspaces
|
||||||
secrets rekey # Re-encrypt everything with a new key
|
secrets list # Show all projects
|
||||||
|
secrets rm <project> # Remove a project's secrets
|
||||||
|
secrets rekey # Re-encrypt everything with a new key
|
||||||
```
|
```
|
||||||
|
|
||||||
If `[project]` is omitted, it's derived from the current directory's git remote or name.
|
If `[project]` is omitted, it's derived from the current directory's git remote or name.
|
||||||
|
|
||||||
|
### Monorepo support
|
||||||
|
|
||||||
|
For monorepos with `package.json` workspaces, use `--workspaces` (`-w`) from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/myapp # has package.json with "workspaces": ["apps/*", "packages/*"]
|
||||||
|
secrets push -w # encrypts .env* from root + each workspace
|
||||||
|
secrets pull -w # decrypts into root + each workspace directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets are stored as `<monorepo>/<workspace-path>/` in `~/.secrets/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.secrets/
|
||||||
|
myapp/
|
||||||
|
.env.age # root
|
||||||
|
apps/web/.env.staging.age # workspace
|
||||||
|
apps/api/.env.age # workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `jq` (`brew install jq`).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -49,5 +94,5 @@ The key file must be copied to each machine once (AirDrop, scp, USB).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install bats-core
|
brew install bats-core
|
||||||
bats test/secrets.bats # 20 tests
|
bats test/secrets.bats # 25 tests
|
||||||
```
|
```
|
||||||
|
|
|
||||||
292
secrets
292
secrets
|
|
@ -46,6 +46,47 @@ derive_project_name() {
|
||||||
basename "$PWD"
|
basename "$PWD"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Collect .env and .env.* files from a directory (excluding .envrc, .environment-*)
|
||||||
|
# Sets the COLLECTED_FILES array. Returns 1 if no files found.
|
||||||
|
collect_env_files() {
|
||||||
|
local dir="$1"
|
||||||
|
COLLECTED_FILES=()
|
||||||
|
for f in "$dir"/.env "$dir"/.env.*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
local basename_f
|
||||||
|
basename_f=$(basename "$f")
|
||||||
|
case "$basename_f" in
|
||||||
|
.envrc|.environment*) continue ;;
|
||||||
|
esac
|
||||||
|
COLLECTED_FILES+=("$f")
|
||||||
|
done
|
||||||
|
[ ${#COLLECTED_FILES[@]} -gt 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read package.json workspaces and expand globs to actual directories.
|
||||||
|
# Prints one workspace path per line (relative to the monorepo root).
|
||||||
|
get_workspaces() {
|
||||||
|
local root="$1"
|
||||||
|
local pkg="$root/package.json"
|
||||||
|
[ -f "$pkg" ] || die "No package.json found in $root"
|
||||||
|
check_cmd jq
|
||||||
|
|
||||||
|
local patterns
|
||||||
|
patterns=$(jq -r '.workspaces // .workspaces.packages // empty | .[]' "$pkg" 2>/dev/null)
|
||||||
|
[ -n "$patterns" ] || die "No workspaces field in $pkg"
|
||||||
|
|
||||||
|
# Expand each glob pattern relative to root
|
||||||
|
local old_dir="$PWD"
|
||||||
|
cd "$root"
|
||||||
|
for pattern in $patterns; do
|
||||||
|
# Use bash glob expansion
|
||||||
|
for dir in $pattern; do
|
||||||
|
[ -d "$dir" ] && echo "$dir"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
cd "$old_dir"
|
||||||
|
}
|
||||||
|
|
||||||
install_hook() {
|
install_hook() {
|
||||||
local hook_src="$SCRIPT_DIR/hooks/pre-commit"
|
local hook_src="$SCRIPT_DIR/hooks/pre-commit"
|
||||||
local hook_dst="$SECRETS_DIR/.git/hooks/pre-commit"
|
local hook_dst="$SECRETS_DIR/.git/hooks/pre-commit"
|
||||||
|
|
@ -116,6 +157,55 @@ EOF
|
||||||
echo " 3. Run 'secrets push <project>' from a project directory"
|
echo " 3. Run 'secrets push <project>' from a project directory"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Encrypt env files from a source dir into a project path in the secrets repo.
|
||||||
|
# Does NOT commit or push — caller handles that.
|
||||||
|
push_dir_to_project() {
|
||||||
|
local source_dir="$1"
|
||||||
|
local project="$2"
|
||||||
|
local pubkey="$3"
|
||||||
|
|
||||||
|
if ! collect_env_files "$source_dir"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "$project: ${#COLLECTED_FILES[@]} file(s)"
|
||||||
|
for f in "${COLLECTED_FILES[@]}"; do
|
||||||
|
echo " $(basename "$f")"
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$SECRETS_DIR/$project"
|
||||||
|
for f in "${COLLECTED_FILES[@]}"; do
|
||||||
|
local name
|
||||||
|
name=$(basename "$f")
|
||||||
|
age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Git commit + push for the secrets repo. Shared by push and push --workspaces.
|
||||||
|
commit_and_push_secrets() {
|
||||||
|
local message="$1"
|
||||||
|
|
||||||
|
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
||||||
|
if ! git -C "$SECRETS_DIR" pull --ff-only 2>/dev/null; then
|
||||||
|
die "Fast-forward pull failed. Run 'secrets pull' first, then retry push."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$SECRETS_DIR" add -A
|
||||||
|
if git -C "$SECRETS_DIR" diff --cached --quiet 2>/dev/null; then
|
||||||
|
info "No changes to push (secrets unchanged)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
git -C "$SECRETS_DIR" commit -m "$message" >/dev/null
|
||||||
|
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
||||||
|
git -C "$SECRETS_DIR" push >/dev/null 2>&1
|
||||||
|
info "Pushed to remote"
|
||||||
|
else
|
||||||
|
info "Committed locally (no remote configured)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd_push() {
|
cmd_push() {
|
||||||
check_cmd age
|
check_cmd age
|
||||||
check_cmd git
|
check_cmd git
|
||||||
|
|
@ -126,59 +216,54 @@ cmd_push() {
|
||||||
project=$(derive_project_name "${1:-}")
|
project=$(derive_project_name "${1:-}")
|
||||||
info "Pushing secrets for project: $project"
|
info "Pushing secrets for project: $project"
|
||||||
|
|
||||||
# Glob .env and .env.* (not .envrc, .environment-*, etc.)
|
|
||||||
local files=()
|
|
||||||
for f in "$PWD"/.env "$PWD"/.env.*; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
local basename_f
|
|
||||||
basename_f=$(basename "$f")
|
|
||||||
# Skip patterns that aren't actual .env files
|
|
||||||
case "$basename_f" in
|
|
||||||
.envrc|.environment*) continue ;;
|
|
||||||
esac
|
|
||||||
files+=("$f")
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#files[@]} -eq 0 ]; then
|
|
||||||
die "No .env or .env.* files found in $PWD"
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Files to encrypt:"
|
|
||||||
for f in "${files[@]}"; do
|
|
||||||
echo " $(basename "$f")"
|
|
||||||
done
|
|
||||||
|
|
||||||
local pubkey
|
local pubkey
|
||||||
pubkey=$(get_pubkey)
|
pubkey=$(get_pubkey)
|
||||||
|
|
||||||
# Encrypt each file
|
if ! push_dir_to_project "$PWD" "$project" "$pubkey"; then
|
||||||
mkdir -p "$SECRETS_DIR/$project"
|
die "No .env or .env.* files found in $PWD"
|
||||||
for f in "${files[@]}"; do
|
fi
|
||||||
local name
|
|
||||||
name=$(basename "$f")
|
|
||||||
age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Pull before push (ff-only)
|
commit_and_push_secrets "update $project"
|
||||||
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
}
|
||||||
if ! git -C "$SECRETS_DIR" pull --ff-only 2>/dev/null; then
|
|
||||||
die "Fast-forward pull failed. Run 'secrets pull $project' first, then retry push."
|
cmd_push_workspaces() {
|
||||||
|
check_cmd age
|
||||||
|
check_cmd git
|
||||||
|
check_cmd jq
|
||||||
|
check_initialized
|
||||||
|
check_key
|
||||||
|
|
||||||
|
local root="$PWD"
|
||||||
|
local monorepo_name
|
||||||
|
monorepo_name=$(derive_project_name "")
|
||||||
|
info "Pushing workspaces for monorepo: $monorepo_name"
|
||||||
|
|
||||||
|
local pubkey
|
||||||
|
pubkey=$(get_pubkey)
|
||||||
|
local total=0
|
||||||
|
|
||||||
|
# Push root env files (if any)
|
||||||
|
if push_dir_to_project "$root" "$monorepo_name" "$pubkey"; then
|
||||||
|
total=$((total + ${#COLLECTED_FILES[@]}))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push each workspace
|
||||||
|
local workspaces
|
||||||
|
workspaces=$(get_workspaces "$root")
|
||||||
|
while IFS= read -r ws; do
|
||||||
|
[ -n "$ws" ] || continue
|
||||||
|
local ws_dir="$root/$ws"
|
||||||
|
local ws_project="$monorepo_name/$ws"
|
||||||
|
if push_dir_to_project "$ws_dir" "$ws_project" "$pubkey"; then
|
||||||
|
total=$((total + ${#COLLECTED_FILES[@]}))
|
||||||
fi
|
fi
|
||||||
|
done <<< "$workspaces"
|
||||||
|
|
||||||
|
if [ "$total" -eq 0 ]; then
|
||||||
|
die "No .env files found in any workspace"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit and push
|
commit_and_push_secrets "update $monorepo_name workspaces"
|
||||||
git -C "$SECRETS_DIR" add "$project/"
|
|
||||||
if git -C "$SECRETS_DIR" diff --cached --quiet 2>/dev/null; then
|
|
||||||
info "No changes to push (secrets unchanged)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
git -C "$SECRETS_DIR" commit -m "update $project" >/dev/null
|
|
||||||
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
|
||||||
git -C "$SECRETS_DIR" push >/dev/null 2>&1
|
|
||||||
info "Pushed $project to remote"
|
|
||||||
else
|
|
||||||
info "Committed $project locally (no remote configured)"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_pull() {
|
cmd_pull() {
|
||||||
|
|
@ -226,6 +311,85 @@ cmd_pull() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Pull and decrypt .age files from a project path into a target directory.
|
||||||
|
# Does NOT do git pull — caller handles that.
|
||||||
|
pull_project_to_dir() {
|
||||||
|
local project="$1"
|
||||||
|
local target_dir="$2"
|
||||||
|
local project_dir="$SECRETS_DIR/$project"
|
||||||
|
|
||||||
|
[ -d "$project_dir" ] || return 1
|
||||||
|
|
||||||
|
local count=0
|
||||||
|
for f in "$project_dir"/*.age "$project_dir"/.*.age; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
local name
|
||||||
|
name=$(basename "$f" .age)
|
||||||
|
local outfile="$target_dir/$name"
|
||||||
|
age -d -i "$KEY_FILE" -o "$outfile" "$f"
|
||||||
|
if [ ! -s "$outfile" ]; then
|
||||||
|
echo "WARNING: Decrypted file '$name' is empty (possibly truncated .age blob)"
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
echo "$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_pull_workspaces() {
|
||||||
|
check_cmd age
|
||||||
|
check_cmd git
|
||||||
|
check_cmd jq
|
||||||
|
check_initialized
|
||||||
|
check_key
|
||||||
|
|
||||||
|
local root="$PWD"
|
||||||
|
local monorepo_name
|
||||||
|
monorepo_name=$(derive_project_name "")
|
||||||
|
info "Pulling workspaces for monorepo: $monorepo_name"
|
||||||
|
|
||||||
|
# Pull latest from remote
|
||||||
|
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
||||||
|
git -C "$SECRETS_DIR" pull >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local total=0
|
||||||
|
|
||||||
|
# Pull root secrets (if any)
|
||||||
|
if [ -d "$SECRETS_DIR/$monorepo_name" ]; then
|
||||||
|
local count
|
||||||
|
count=$(pull_project_to_dir "$monorepo_name" "$root")
|
||||||
|
total=$((total + count))
|
||||||
|
info "$monorepo_name (root): $count file(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull each workspace
|
||||||
|
local workspaces
|
||||||
|
workspaces=$(get_workspaces "$root")
|
||||||
|
while IFS= read -r ws; do
|
||||||
|
[ -n "$ws" ] || continue
|
||||||
|
local ws_dir="$root/$ws"
|
||||||
|
local ws_project="$monorepo_name/$ws"
|
||||||
|
if [ -d "$SECRETS_DIR/$ws_project" ]; then
|
||||||
|
local count
|
||||||
|
count=$(pull_project_to_dir "$ws_project" "$ws_dir")
|
||||||
|
total=$((total + count))
|
||||||
|
info "$ws_project: $count file(s)"
|
||||||
|
fi
|
||||||
|
done <<< "$workspaces"
|
||||||
|
|
||||||
|
if [ "$total" -eq 0 ]; then
|
||||||
|
die "No secrets found for any workspace in $monorepo_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Decrypted $total file(s) total"
|
||||||
|
|
||||||
|
# Reinstall hook if missing
|
||||||
|
if [ ! -x "$SECRETS_DIR/.git/hooks/pre-commit" ]; then
|
||||||
|
install_hook
|
||||||
|
info "Reinstalled pre-commit hook"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
check_initialized
|
check_initialized
|
||||||
|
|
||||||
|
|
@ -354,16 +518,24 @@ cmd_help() {
|
||||||
secrets — encrypted env file sync between machines
|
secrets — encrypted env file sync between machines
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
secrets init Initialize the secrets repo and generate an age key
|
secrets init Initialize the secrets repo and generate an age key
|
||||||
secrets push [project] Encrypt .env* files and push to the secrets repo
|
secrets push [project] Encrypt .env* files and push to the secrets repo
|
||||||
secrets pull [project] Pull and decrypt .env* files into current directory
|
secrets push -w|--workspaces Push .env* from all workspaces in package.json
|
||||||
secrets list List all projects and their secret files
|
secrets pull [project] Pull and decrypt .env* files into current directory
|
||||||
secrets rm <project> Remove a project's secrets from the repo
|
secrets pull -w|--workspaces Pull .env* into all workspaces from package.json
|
||||||
secrets rekey Re-encrypt all secrets with a new key
|
secrets list List all projects and their secret files
|
||||||
|
secrets rm <project> Remove a project's secrets from the repo
|
||||||
|
secrets rekey Re-encrypt all secrets with a new key
|
||||||
|
|
||||||
If [project] is omitted, it is derived from the current directory's
|
If [project] is omitted, it is derived from the current directory's
|
||||||
git remote (if available) or the directory name.
|
git remote (if available) or the directory name.
|
||||||
|
|
||||||
|
Workspaces:
|
||||||
|
With -w/--workspaces, reads package.json "workspaces" field to find
|
||||||
|
workspace directories. Each workspace's .env* files are stored under
|
||||||
|
<monorepo>/<workspace-path>/ in the secrets repo. Root .env* files
|
||||||
|
are stored under <monorepo>/ directly. Requires jq.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
SECRETS_DIR Path to secrets repo (default: ~/.secrets)
|
SECRETS_DIR Path to secrets repo (default: ~/.secrets)
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -373,8 +545,20 @@ EOF
|
||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
init) cmd_init ;;
|
init) cmd_init ;;
|
||||||
push) cmd_push "${2:-}" ;;
|
push)
|
||||||
pull) cmd_pull "${2:-}" ;;
|
if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then
|
||||||
|
cmd_push_workspaces
|
||||||
|
else
|
||||||
|
cmd_push "${2:-}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
pull)
|
||||||
|
if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then
|
||||||
|
cmd_pull_workspaces
|
||||||
|
else
|
||||||
|
cmd_pull "${2:-}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
list) cmd_list ;;
|
list) cmd_list ;;
|
||||||
rm) cmd_rm "${2:-}" ;;
|
rm) cmd_rm "${2:-}" ;;
|
||||||
rekey) cmd_rekey ;;
|
rekey) cmd_rekey ;;
|
||||||
|
|
|
||||||
|
|
@ -251,3 +251,102 @@ load test_helper
|
||||||
run git commit -m "should succeed"
|
run git commit -m "should succeed"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ─── workspaces ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Helper: create a monorepo with package.json workspaces
|
||||||
|
create_monorepo() {
|
||||||
|
local dir="$WORK_DIR/myapp"
|
||||||
|
mkdir -p "$dir/apps/web" "$dir/apps/api" "$dir/packages/auth"
|
||||||
|
|
||||||
|
cat > "$dir/package.json" << 'PKGJSON'
|
||||||
|
{
|
||||||
|
"name": "myapp",
|
||||||
|
"workspaces": ["apps/*", "packages/*"]
|
||||||
|
}
|
||||||
|
PKGJSON
|
||||||
|
|
||||||
|
# Root env
|
||||||
|
echo "ROOT_SECRET=top" > "$dir/.env"
|
||||||
|
# Workspace envs
|
||||||
|
echo "WEB_DB=webdb" > "$dir/apps/web/.env.staging"
|
||||||
|
echo "API_KEY=abc" > "$dir/apps/api/.env"
|
||||||
|
# packages/auth has no .env — should be skipped silently
|
||||||
|
|
||||||
|
# Init a git repo so derive_project_name can use dirname
|
||||||
|
git init "$dir" >/dev/null 2>&1
|
||||||
|
echo "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "push --workspaces encrypts root and workspace env files" {
|
||||||
|
init_with_remote
|
||||||
|
local mono
|
||||||
|
mono=$(create_monorepo)
|
||||||
|
cd "$mono"
|
||||||
|
|
||||||
|
run "$SECRETS_BIN" push --workspaces
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Root env
|
||||||
|
[ -f "$SECRETS_DIR/myapp/.env.age" ]
|
||||||
|
# Workspace envs
|
||||||
|
[ -f "$SECRETS_DIR/myapp/apps/web/.env.staging.age" ]
|
||||||
|
[ -f "$SECRETS_DIR/myapp/apps/api/.env.age" ]
|
||||||
|
# packages/auth should NOT have a dir (no .env files)
|
||||||
|
[ ! -d "$SECRETS_DIR/myapp/packages/auth" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "pull --workspaces decrypts into correct directories" {
|
||||||
|
init_with_remote
|
||||||
|
local mono
|
||||||
|
mono=$(create_monorepo)
|
||||||
|
cd "$mono"
|
||||||
|
"$SECRETS_BIN" push --workspaces >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Remove the original env files
|
||||||
|
rm "$mono/.env" "$mono/apps/web/.env.staging" "$mono/apps/api/.env"
|
||||||
|
|
||||||
|
run "$SECRETS_BIN" pull --workspaces
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Verify decrypted into correct locations
|
||||||
|
[ "$(cat "$mono/.env")" = "ROOT_SECRET=top" ]
|
||||||
|
[ "$(cat "$mono/apps/web/.env.staging")" = "WEB_DB=webdb" ]
|
||||||
|
[ "$(cat "$mono/apps/api/.env")" = "API_KEY=abc" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "push --workspaces errors without package.json" {
|
||||||
|
init_with_remote
|
||||||
|
mkdir -p "$WORK_DIR/nopkg"
|
||||||
|
cd "$WORK_DIR/nopkg"
|
||||||
|
|
||||||
|
run "$SECRETS_BIN" push --workspaces
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"No package.json"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "push --workspaces errors without workspaces field" {
|
||||||
|
init_with_remote
|
||||||
|
mkdir -p "$WORK_DIR/nows"
|
||||||
|
echo '{"name": "nows"}' > "$WORK_DIR/nows/package.json"
|
||||||
|
cd "$WORK_DIR/nows"
|
||||||
|
|
||||||
|
run "$SECRETS_BIN" push --workspaces
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"No workspaces"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "push --workspaces errors when no env files anywhere" {
|
||||||
|
init_with_remote
|
||||||
|
local dir="$WORK_DIR/empty-mono"
|
||||||
|
mkdir -p "$dir/apps/web" "$dir/packages/lib"
|
||||||
|
cat > "$dir/package.json" << 'EOF'
|
||||||
|
{"workspaces": ["apps/*", "packages/*"]}
|
||||||
|
EOF
|
||||||
|
git init "$dir" >/dev/null 2>&1
|
||||||
|
cd "$dir"
|
||||||
|
|
||||||
|
run "$SECRETS_BIN" push --workspaces
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"No .env files"* ]]
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue