Workspace patterns came from package.json's `workspaces` key only, at both call sites. pnpm declares them in pnpm-workspace.yaml instead, so no pnpm monorepo ever resolved a workspace: `push -w` refused outright, and plain `push` failed silently — _maybe_workspace_env_files returned 0 the moment the key was absent, leaving the auto-discovery that covers push's root-only scan inert and printing "Nothing new to add", indistinguishable from a repo with genuinely nothing new. That silence cost two sessions on the same repo. Both call sites now resolve through one shared source, _workspace_patterns: package.json when it declares any, else pnpm-workspace.yaml's `packages:` block. The YAML read is deliberately not a parser — block sequence only, stopping at the next top-level key so pnpm 10's onlyBuiltDependencies:/ catalog: cannot leak in as globs, with quote/comment handling and a symlink refusal. Also fixes yarn's object form. `.workspaces // .workspaces.packages | .[]` short-circuits on the truthy object, iterating its values and yielding the pattern array as one token; only npm's array form ever worked. Note the obvious reorder is NOT the fix — `.workspaces.packages` errors on an array — so the filter is type-aware. Patterns are validated before reaching the unquoted glob expansion (no absolute paths, `..`, metacharacters, or whitespace; pnpm `!` negations skipped), matching the .secrets-store/.secrets-files posture. jq is now required only when package.json is the source. A monorepo-shaped root that resolves nothing warns and points at `secrets add` instead of returning in silence, and `-w`'s error names pnpm-workspace.yaml when that is the file present. Scope note: the workspace re-scan still runs only for projects that already have a .secrets.json — push's root-scan-only first push is by design (EGB-677 E13), and this bug is the fallback covering it never engaging. test/workspaces.bats: 18 new tests. Full suite 371/371 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
300 lines
9.2 KiB
Bash
300 lines
9.2 KiB
Bash
#!/usr/bin/env bats
|
|
# EGB-1232: workspace discovery must not be npm-only.
|
|
#
|
|
# Two defects, both in the pattern-source layer:
|
|
# 1. pnpm declares workspaces in pnpm-workspace.yaml, not package.json's
|
|
# `workspaces` key — so both call sites came up empty on every pnpm
|
|
# monorepo (silently, in the plain-push path).
|
|
# 2. the jq filter `.workspaces // .workspaces.packages` short-circuits on
|
|
# yarn's truthy object form, iterating the object's values and yielding
|
|
# the pattern array itself instead of the patterns.
|
|
#
|
|
# bash 3.2 gotcha (see CLAUDE.md): every standalone [[ ]] assertion MUST end
|
|
# with `|| false`, or a failing assertion does not fail the test.
|
|
|
|
load test_helper
|
|
|
|
# ─── fixtures ──────────────────────────────────────────────────────────
|
|
|
|
# A monorepo root with two workspace packages, each holding a .env.
|
|
# The workspace *declaration* is left to the caller — that is what varies.
|
|
# NB: the workspace re-scan (_maybe_workspace_env_files) runs only for
|
|
# projects that already have a .secrets.json — push's root-scan-only
|
|
# behaviour on a first push is by design (EGB-677 E13), and EGB-1232 is
|
|
# about the fallback that covers it never engaging. So every fixture
|
|
# carries a manifest, matching the reported repro.
|
|
make_monorepo() {
|
|
local dir="$WORK_DIR/${1:-mono}"
|
|
mkdir -p "$dir/apps/web" "$dir/apps/admin"
|
|
echo "VITE_CLERK_PUBLISHABLE_KEY=pk_test_web" > "$dir/apps/web/.env"
|
|
echo "VITE_CLERK_PUBLISHABLE_KEY=pk_test_admin" > "$dir/apps/admin/.env"
|
|
printf '{\n "version": 2,\n "dotenv": [],\n "external": []\n}\n' > "$dir/.secrets.json"
|
|
cd "$dir"
|
|
}
|
|
|
|
declare_pnpm() {
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "apps/*"
|
|
- "packages/*"
|
|
YAML
|
|
echo '{"name":"mono","private":true}' > package.json
|
|
}
|
|
|
|
declare_npm() {
|
|
echo '{"name":"mono","private":true,"workspaces":["apps/*"]}' > package.json
|
|
}
|
|
|
|
declare_yarn_object() {
|
|
echo '{"name":"mono","private":true,"workspaces":{"packages":["apps/*"]}}' > package.json
|
|
}
|
|
|
|
# ─── the pnpm gap (the reported bug) ───────────────────────────────────
|
|
|
|
@test "EGB-1232: push discovers workspace env files in a pnpm monorepo" {
|
|
init_with_remote
|
|
make_monorepo pnpmrepo
|
|
declare_pnpm
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" == *"apps/admin/.env"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: push actually vaults pnpm workspace env files" {
|
|
init_with_remote
|
|
make_monorepo pnpmreal
|
|
declare_pnpm
|
|
|
|
run "$SECRETS_BIN" push
|
|
[ "$status" -eq 0 ]
|
|
|
|
rm -f apps/web/.env apps/admin/.env
|
|
run "$SECRETS_BIN" pull
|
|
[ "$status" -eq 0 ]
|
|
[ -f apps/web/.env ]
|
|
[ -f apps/admin/.env ]
|
|
# The value that was empty in the .env.example — the actual impact.
|
|
[[ "$(cat apps/web/.env)" == *"pk_test_web"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: push -w works on a pnpm monorepo" {
|
|
init_with_remote
|
|
make_monorepo pnpmw
|
|
declare_pnpm
|
|
|
|
run "$SECRETS_BIN" push -w
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"No workspaces field"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: pnpm-workspace.yaml is read without requiring a package.json" {
|
|
init_with_remote
|
|
make_monorepo pnpmnopkg
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "apps/*"
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
}
|
|
|
|
# ─── pnpm YAML parsing edge cases ──────────────────────────────────────
|
|
|
|
@test "EGB-1232: pnpm parsing stops at the next top-level key" {
|
|
init_with_remote
|
|
make_monorepo pnpmkeys
|
|
# pnpm 10+ carries sibling top-level keys. They must not leak in as globs.
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "apps/*"
|
|
|
|
onlyBuiltDependencies:
|
|
- esbuild
|
|
- sharp
|
|
|
|
catalog:
|
|
react: ^18.0.0
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" != *"esbuild"* ]] || false
|
|
[[ "$output" != *"sharp"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: pnpm parsing handles quotes, bare entries, and comments" {
|
|
init_with_remote
|
|
make_monorepo pnpmquotes
|
|
mkdir -p docs && echo "DOCS=1" > docs/.env
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
# which packages belong to this workspace
|
|
packages:
|
|
- "apps/*" # double quoted, with a trailing comment
|
|
- 'docs' # single quoted, no glob
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" == *"docs/.env"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: a pnpm-workspace.yaml with no packages key discovers nothing and says so" {
|
|
init_with_remote
|
|
make_monorepo pnpmempty
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
onlyBuiltDependencies:
|
|
- esbuild
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"pnpm-workspace.yaml"* ]] || false
|
|
}
|
|
|
|
# ─── the yarn object-form defect ───────────────────────────────────────
|
|
|
|
@test "EGB-1232: yarn's object workspaces form is expanded, not iterated" {
|
|
init_with_remote
|
|
make_monorepo yarnobj
|
|
declare_yarn_object
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" == *"apps/admin/.env"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: yarn object form works under -w" {
|
|
init_with_remote
|
|
make_monorepo yarnobjw
|
|
declare_yarn_object
|
|
|
|
run "$SECRETS_BIN" push -w
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"No workspaces field"* ]] || false
|
|
}
|
|
|
|
# ─── npm regression guard ──────────────────────────────────────────────
|
|
|
|
@test "EGB-1232: npm's array workspaces form still works" {
|
|
init_with_remote
|
|
make_monorepo npmarr
|
|
declare_npm
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" == *"apps/admin/.env"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: package.json workspaces wins when both sources are present" {
|
|
init_with_remote
|
|
make_monorepo bothsrc
|
|
mkdir -p only-pnpm && echo "P=1" > only-pnpm/.env
|
|
echo '{"name":"mono","workspaces":["apps/*"]}' > package.json
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "only-pnpm"
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"apps/web/.env"* ]] || false
|
|
[[ "$output" != *"only-pnpm/.env"* ]] || false
|
|
}
|
|
|
|
# ─── diagnostics: the silence is the bug ───────────────────────────────
|
|
|
|
@test "EGB-1232: a monorepo-shaped root with no readable workspace source warns" {
|
|
init_with_remote
|
|
make_monorepo shaped
|
|
# Looks like a monorepo (pnpm-workspace.yaml present) but declares nothing.
|
|
echo "# nothing useful here" > pnpm-workspace.yaml
|
|
# A root .env so the push SUCCEEDS — the dangerous case is a green push
|
|
# that silently skipped every workspace, which is what bit thatsbait.
|
|
echo "ROOT=1" > .env
|
|
|
|
run "$SECRETS_BIN" push
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"WARNING"* ]] || false
|
|
[[ "$output" == *"workspace"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: the warning tells you how to recover" {
|
|
init_with_remote
|
|
make_monorepo shapedfix
|
|
echo "# nothing useful here" > pnpm-workspace.yaml
|
|
echo "ROOT=1" > .env
|
|
|
|
run "$SECRETS_BIN" push
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"secrets add"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: a plain single-package project never warns about workspaces" {
|
|
init_with_remote
|
|
create_project_dir plainproj
|
|
printf '{\n "version": 2,\n "dotenv": [],\n "external": []\n}\n' > .secrets.json
|
|
|
|
run "$SECRETS_BIN" push
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"workspace"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: push -w names pnpm-workspace.yaml when that is the file present" {
|
|
init_with_remote
|
|
make_monorepo wnames
|
|
echo '{"name":"mono"}' > package.json
|
|
echo "# no packages key" > pnpm-workspace.yaml
|
|
|
|
run "$SECRETS_BIN" push -w
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"pnpm-workspace.yaml"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: push -w still names package.json when no pnpm file exists" {
|
|
init_with_remote
|
|
make_monorepo wnamespkg
|
|
echo '{"name":"mono"}' > package.json
|
|
|
|
run "$SECRETS_BIN" push -w
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"package.json"* ]] || false
|
|
}
|
|
|
|
# ─── path safety ───────────────────────────────────────────────────────
|
|
|
|
@test "EGB-1232: a pnpm packages entry cannot escape the project root" {
|
|
init_with_remote
|
|
make_monorepo escapee
|
|
echo "OUTSIDE=1" > "$WORK_DIR/outside.env"
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "../"
|
|
- "/etc"
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"outside.env"* ]] || false
|
|
[[ "$output" != *"/etc/"* ]] || false
|
|
}
|
|
|
|
@test "EGB-1232: a pnpm packages entry with shell metacharacters is refused" {
|
|
init_with_remote
|
|
make_monorepo metachars
|
|
cat > pnpm-workspace.yaml <<'YAML'
|
|
packages:
|
|
- "apps/*; touch /tmp/egb1232-pwned"
|
|
YAML
|
|
|
|
run "$SECRETS_BIN" push --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[ ! -f /tmp/egb1232-pwned ]
|
|
}
|