#!/usr/bin/env bats # EGB-677 stage 1: .secrets.json manifest — parse, rails, add, generators. load test_helper # ─── A: manifest core — secrets add + rails + canonical form ────────── @test "add creates .secrets.json with version 2 and the dotenv entry" { create_project_dir addproj run "$SECRETS_BIN" add .env [ "$status" -eq 0 ] [ -f ".secrets.json" ] run jq -r '.version' .secrets.json [ "$output" = "2" ] run jq -r '.dotenv[0]' .secrets.json [ "$output" = ".env" ] } @test "add is idempotent — no duplicate entries" { create_project_dir addproj "$SECRETS_BIN" add .env >/dev/null run "$SECRETS_BIN" add .env [ "$status" -eq 0 ] run jq -r '.dotenv | length' .secrets.json [ "$output" = "1" ] } @test "add accepts nested workspace paths" { create_project_dir addproj mkdir -p packages/web echo "K=v" > packages/web/.env.development run "$SECRETS_BIN" add packages/web/.env.development [ "$status" -eq 0 ] run jq -r '.dotenv | index("packages/web/.env.development") != null' .secrets.json [ "$output" = "true" ] } @test "add accepts npm-scoped workspace paths (@)" { create_project_dir addproj mkdir -p "packages/@acme/web" echo "K=v" > "packages/@acme/web/.env" run "$SECRETS_BIN" add "packages/@acme/web/.env" [ "$status" -eq 0 ] run jq -r '.dotenv | index("packages/@acme/web/.env") != null' .secrets.json [ "$output" = "true" ] } @test "add rejects path traversal (..)" { create_project_dir addproj run "$SECRETS_BIN" add ../escape/.env [ "$status" -eq 1 ] [[ "$output" == *"project-relative"* ]] || false [ ! -f ".secrets.json" ] } @test "add rejects absolute paths" { create_project_dir addproj run "$SECRETS_BIN" add /etc/passwd [ "$status" -eq 1 ] [[ "$output" == *"project-relative"* ]] || false [ ! -f ".secrets.json" ] } @test "add rejects shell metacharacters in path" { create_project_dir addproj run "$SECRETS_BIN" add '.env;rm -rf ~' [ "$status" -eq 1 ] [ ! -f ".secrets.json" ] } @test "add requires the file to exist" { create_project_dir addproj run "$SECRETS_BIN" add .env.missing [ "$status" -eq 1 ] [[ "$output" == *"not found"* ]] || false } @test "manifest serialization is canonical — order of adds does not matter" { create_project_dir addproj echo "A=1" > .env.alpha echo "B=2" > .env.beta "$SECRETS_BIN" add .env.alpha >/dev/null "$SECRETS_BIN" add .env.beta >/dev/null cp .secrets.json "$TEST_TMPDIR/order1.json" rm .secrets.json "$SECRETS_BIN" add .env.beta >/dev/null "$SECRETS_BIN" add .env.alpha >/dev/null cmp -s .secrets.json "$TEST_TMPDIR/order1.json" } @test "which shows manifest summary when .secrets.json is present" { create_project_dir addproj "$SECRETS_BIN" add .env >/dev/null run "$SECRETS_BIN" which [ "$status" -eq 0 ] [[ "$output" == *".secrets.json"* ]] || false [[ "$output" == *".env"* ]] || false } @test "malformed .secrets.json dies with a directed error naming the file" { create_project_dir addproj echo '{ not json' > .secrets.json run "$SECRETS_BIN" which [ "$status" -eq 1 ] [[ "$output" == *".secrets.json"* ]] || false [[ "$output" == *"invalid"* ]] || false } @test "unsupported manifest version dies with a directed upgrade error" { create_project_dir addproj echo '{"version": 99, "dotenv": [".env"]}' > .secrets.json run "$SECRETS_BIN" which [ "$status" -eq 1 ] [[ "$output" == *"version 99"* ]] || false [[ "$output" == *"supports"* ]] || false } @test "symlinked .secrets.json is refused" { create_project_dir addproj echo '{"version":2,"dotenv":[".env"]}' > "$TEST_TMPDIR/real-manifest.json" ln -s "$TEST_TMPDIR/real-manifest.json" .secrets.json run "$SECRETS_BIN" which [ "$status" -eq 1 ] [[ "$output" == *"symlink"* ]] || false } # ─── B: push from manifest — generators, autoAdd, --frozen/--dry-run ─── @test "push with manifest syncs nested declared file into v1 store layout" { init_with_remote create_project_dir nestproj mkdir -p packages/web echo "K=v" > packages/web/.env.development "$SECRETS_BIN" add packages/web/.env.development >/dev/null run "$SECRETS_BIN" push [ "$status" -eq 0 ] [ -f "$SECRETS_DIR/nestproj/packages/web/.env.development.age" ] } @test "push auto-adds newly discovered root files to an existing manifest" { init_with_remote create_project_dir autoproj "$SECRETS_BIN" add .env >/dev/null run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *"Added"* ]] || false run jq -r '.dotenv | index(".env.staging") != null' .secrets.json [ "$output" = "true" ] [ -f "$SECRETS_DIR/autoproj/.env.staging.age" ] } @test "bootstrap: plain push creates the manifest from discovered files" { init_with_remote create_project_dir bootproj run "$SECRETS_BIN" push [ "$status" -eq 0 ] [ -f ".secrets.json" ] run jq -r '.dotenv | length' .secrets.json [ "$output" = "2" ] } @test "failed push leaves no bootstrap manifest behind" { init_with_remote mkdir -p "$WORK_DIR/emptyproj" cd "$WORK_DIR/emptyproj" run "$SECRETS_BIN" push [ "$status" -eq 1 ] [ ! -f ".secrets.json" ] } @test "autoAdd=false: undeclared discovered file is warned about, not added or synced" { init_with_remote create_project_dir noaddproj printf '{"version":2,"options":{"autoAdd":false},"dotenv":[".env"]}\n' > .secrets.json run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *"not declared"* ]] || false run jq -r '.dotenv | index(".env.staging") != null' .secrets.json [ "$output" = "false" ] [ -f "$SECRETS_DIR/noaddproj/.env.age" ] [ ! -f "$SECRETS_DIR/noaddproj/.env.staging.age" ] } @test "push --frozen skips auto-add even when autoAdd is on" { init_with_remote create_project_dir frozenproj "$SECRETS_BIN" add .env >/dev/null run "$SECRETS_BIN" push --frozen [ "$status" -eq 0 ] run jq -r '.dotenv | index(".env.staging") != null' .secrets.json [ "$output" = "false" ] [ ! -f "$SECRETS_DIR/frozenproj/.env.staging.age" ] # declared entry still synced under the REAL project name [ -f "$SECRETS_DIR/frozenproj/.env.age" ] } @test "push --dry-run reports would-add entries and changes nothing" { init_with_remote create_project_dir dryproj "$SECRETS_BIN" add .env >/dev/null cp .secrets.json "$TEST_TMPDIR/manifest-before.json" run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *".env.staging"* ]] || false cmp -s .secrets.json "$TEST_TMPDIR/manifest-before.json" [ ! -f "$SECRETS_DIR/dryproj/.env.age" ] # nothing committed to the store at all [ "$(git -C "$SECRETS_DIR" rev-list --count HEAD)" -eq 1 ] } @test "plain push re-scans package.json workspaces when a manifest exists" { init_with_remote local mono="$WORK_DIR/wsproj" mkdir -p "$mono/packages/api" printf '{"workspaces": ["packages/*"]}\n' > "$mono/package.json" echo "ROOT=1" > "$mono/.env" echo "API=1" > "$mono/packages/api/.dev.vars" git init "$mono" >/dev/null 2>&1 cd "$mono" "$SECRETS_BIN" add .env >/dev/null run "$SECRETS_BIN" push [ "$status" -eq 0 ] run jq -r '.dotenv | index("packages/api/.dev.vars") != null' .secrets.json [ "$output" = "true" ] [ -f "$SECRETS_DIR/wsproj/packages/api/.dev.vars.age" ] } @test "declared-but-missing file warns and push continues" { init_with_remote create_project_dir missproj "$SECRETS_BIN" add .env >/dev/null printf '{"version":2,"dotenv":[".env",".env.gone"]}\n' > .secrets.json run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *".env.gone"* ]] || false [ -f "$SECRETS_DIR/missproj/.env.age" ] } @test "unsafe dotenv entry in a committed manifest dies on push" { init_with_remote create_project_dir evilproj printf '{"version":2,"dotenv":["../escape/.env"]}\n' > .secrets.json run "$SECRETS_BIN" push [ "$status" -eq 1 ] [[ "$output" == *"project-relative"* ]] || false } # ─── C: legacy absorb + external entries via .secrets.json ───────────── # Local fixtures (mirror secrets.bats EGB-531/652 helpers) m_gradle_src() { mkdir -p "$HOME/.gradle"; printf '%s' "$1" > "$HOME/.gradle/gradle.properties"; } m_file_src() { mkdir -p "$HOME/keystores"; printf 'KS\x00\x01\x02\xffDATA\n' > "$HOME/keystores/upload.keystore"; } @test "push absorbs .secrets-files into .secrets.json (properties + file)" { init_with_remote m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' m_file_src local dir="$WORK_DIR/absorbproj"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\nfile ~/keystores/upload.keystore\n' > "$dir/.secrets-files" cd "$dir" run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *"Absorbed"* ]] || false run jq -r '.external | length' .secrets.json [ "$output" = "2" ] run jq -r '.external[] | select(.path == "~/.gradle/gradle.properties") | .type' .secrets.json [ "$output" = "properties" ] run jq -r '.external[] | select(.type == "file") | .path' .secrets.json [ "$output" = "~/keystores/upload.keystore" ] # stage 1: blob naming stays legacy-compatible run bash -c "ls $SECRETS_DIR/absorbproj/external/*.gradle-properties.age" [ "$status" -eq 0 ] } @test "absorb is idempotent — second push adds no duplicate externals" { init_with_remote m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' local dir="$WORK_DIR/absorb2"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$dir/.secrets-files" cd "$dir" "$SECRETS_BIN" push >/dev/null 2>&1 run "$SECRETS_BIN" push [ "$status" -eq 0 ] run jq -r '.external | length' .secrets.json [ "$output" = "1" ] } @test "external properties entry in .secrets.json drives push without .secrets-files" { init_with_remote m_gradle_src $'beaconClerkPkTest=pk_test_abc\n' local dir="$WORK_DIR/jsonextproj"; mkdir -p "$dir" printf '{"version":2,"external":[{"type":"properties","path":"~/.gradle/gradle.properties","keys":["beaconClerkPkTest"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push jsonextproj [ "$status" -eq 0 ] [[ "$output" == *"Extracted 1 key"* ]] || false run bash -c "ls $SECRETS_DIR/jsonextproj/external/*.gradle-properties.age" [ "$status" -eq 0 ] } @test "pull merges properties keys sourced from .secrets.json" { init_with_remote m_gradle_src $'beaconClerkPkTest=pk_test_abc\nunrelated=keep\n' local dir="$WORK_DIR/jsonpull"; mkdir -p "$dir" printf '{"version":2,"external":[{"type":"properties","path":"~/.gradle/gradle.properties","keys":["beaconClerkPkTest"]}]}\n' > "$dir/.secrets.json" cd "$dir" "$SECRETS_BIN" push jsonpull >/dev/null 2>&1 m_gradle_src $'beaconClerkPkTest=STALE\nunrelated=keep\n' run "$SECRETS_BIN" pull jsonpull [ "$status" -eq 0 ] run grep -c 'beaconClerkPkTest=pk_test_abc' "$HOME/.gradle/gradle.properties" [ "$output" = "1" ] run grep -c 'unrelated=keep' "$HOME/.gradle/gradle.properties" [ "$output" = "1" ] } @test "properties rail generalized: any *.properties basename is accepted" { init_with_remote mkdir -p "$HOME/.config" printf 'apiKey=abc123\n' > "$HOME/.config/app.properties" local dir="$WORK_DIR/genprops"; mkdir -p "$dir" printf '{"version":2,"external":[{"type":"properties","path":"~/.config/app.properties","keys":["apiKey"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push genprops [ "$status" -eq 0 ] [[ "$output" == *"Extracted 1 key"* ]] || false } @test "properties rail still blocks a non-.properties target" { init_with_remote printf 'PATH=/evil\n' > "$HOME/.bashrc" local dir="$WORK_DIR/evilprops"; mkdir -p "$dir" printf '{"version":2,"external":[{"type":"properties","path":"~/.bashrc","keys":["PATH"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push evilprops [ "$status" -eq 1 ] [[ "$output" == *".properties"* ]] || false } @test "file entry via .secrets.json round-trips binary with mode 600" { init_with_remote m_file_src local dir="$WORK_DIR/jsonfile"; mkdir -p "$dir" printf '{"version":2,"external":[{"type":"file","path":"~/keystores/upload.keystore"}]}\n' > "$dir/.secrets.json" cd "$dir" "$SECRETS_BIN" push jsonfile >/dev/null 2>&1 cp "$HOME/keystores/upload.keystore" "$TEST_TMPDIR/orig.keystore" rm "$HOME/keystores/upload.keystore" run "$SECRETS_BIN" pull jsonfile [ "$status" -eq 0 ] cmp -s "$HOME/keystores/upload.keystore" "$TEST_TMPDIR/orig.keystore" local mode mode=$(stat -f '%Lp' "$HOME/keystores/upload.keystore" 2>/dev/null || stat -c '%a' "$HOME/keystores/upload.keystore") [ "$mode" = "600" ] } @test "json file entry with keys is rejected with a warning" { init_with_remote m_file_src local dir="$WORK_DIR/badfile"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf '{"version":2,"external":[{"type":"file","path":"~/keystores/upload.keystore","keys":["nope"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push badfile [ "$status" -eq 0 ] [[ "$output" == *"no keys"* ]] || false run bash -c "ls $SECRETS_DIR/badfile/external/*.file.age 2>/dev/null" [ "$status" -ne 0 ] } @test "pull warns that .secrets-files is superseded when .secrets.json exists" { init_with_remote create_project_dir superproj "$SECRETS_BIN" push superproj >/dev/null 2>&1 printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files run "$SECRETS_BIN" pull superproj [ "$status" -eq 0 ] [[ "$output" == *"superseded"* ]] || false } # ─── D: pull from manifest — nested restore, restore-time rail ───────── @test "pull restores manifest-declared nested file (mkdir -p)" { init_with_remote create_project_dir nestpull mkdir -p packages/web echo "K=v" > packages/web/.env.development "$SECRETS_BIN" add packages/web/.env.development >/dev/null "$SECRETS_BIN" push >/dev/null 2>&1 rm -rf packages run "$SECRETS_BIN" pull [ "$status" -eq 0 ] [ -f packages/web/.env.development ] [ "$(cat packages/web/.env.development)" = "K=v" ] } @test "pull with manifest restores only declared entries" { init_with_remote create_project_dir onlydecl "$SECRETS_BIN" push >/dev/null 2>&1 # plant an undeclared stray blob in the store local pubkey; pubkey=$(age-keygen -y "$SECRETS_DIR/key.txt") echo "S=1" | age -r "$pubkey" -o "$SECRETS_DIR/onlydecl/.env.stray.age" rm -f .env .env.staging run "$SECRETS_BIN" pull [ "$status" -eq 0 ] [ -f .env ] [ ! -f .env.stray ] } @test "pull warns and skips an unsafe manifest entry, restores the rest" { init_with_remote create_project_dir unsafepull "$SECRETS_BIN" add .env >/dev/null "$SECRETS_BIN" push >/dev/null 2>&1 printf '{"version":2,"dotenv":[".env","../escape/.env"]}\n' > .secrets.json rm -f .env run "$SECRETS_BIN" pull [ "$status" -eq 0 ] [[ "$output" == *"skipping unsafe"* ]] || false [ -f .env ] [ ! -f "$WORK_DIR/escape/.env" ] } @test "pull on an empty manifest is a warn no-op" { init_with_remote mkdir -p "$WORK_DIR/emptypull" cd "$WORK_DIR/emptypull" printf '{"version":2,"dotenv":[]}\n' > .secrets.json run "$SECRETS_BIN" pull emptypull [ "$status" -eq 0 ] [[ "$output" == *"declares nothing"* ]] || false } @test "pull warns when a declared entry has no blob in the store" { init_with_remote create_project_dir nopullblob "$SECRETS_BIN" push >/dev/null 2>&1 jq '.dotenv += [".env.missing"]' .secrets.json > .secrets.json.tmp && mv .secrets.json.tmp .secrets.json run "$SECRETS_BIN" pull [ "$status" -eq 0 ] [[ "$output" == *".env.missing"* ]] || false [[ "$output" == *"no encrypted data"* ]] || false } @test "machine-2 flow: committed manifest + pull restores everything" { init_with_remote create_project_dir machine1 mkdir -p packages/api echo "API=1" > packages/api/.dev.vars "$SECRETS_BIN" add packages/api/.dev.vars >/dev/null "$SECRETS_BIN" push m2proj >/dev/null 2>&1 # simulate machine 2: fresh dir, only the committed manifest present mkdir -p "$WORK_DIR/machine2" cp .secrets.json "$WORK_DIR/machine2/" cd "$WORK_DIR/machine2" run "$SECRETS_BIN" pull m2proj [ "$status" -eq 0 ] [ -f .env ] [ -f packages/api/.dev.vars ] [ "$(cat packages/api/.dev.vars)" = "API=1" ] } # ─── E: jq gating + install hints + help ─────────────────────────────── # Helper: PATH with age but without jq. macOS ships /usr/bin/jq, so # /usr/bin must be excluded too — needed tools are symlinked explicitly. m_nojq_path() { local fake="$TEST_TMPDIR/nojq-bin" mkdir -p "$fake" local t for t in age age-keygen git basename dirname mktemp grep sed tr cut cksum stat head tail sort uniq wc env touch find diff cmp; do command -v "$t" >/dev/null 2>&1 && ln -sf "$(command -v "$t")" "$fake/$t" done rm -f "$fake/jq" echo "$fake:/bin" } @test "manifest-less push works without jq (manifest features skipped)" { init_with_remote create_project_dir nojqproj local p; p=$(m_nojq_path) run env PATH="$p" "$SECRETS_BIN" push [ "$status" -eq 0 ] [ -f "$SECRETS_DIR/nojqproj/.env.age" ] [ ! -f ".secrets.json" ] [[ "$output" == *"jq"* ]] || false } @test "push dies with an install hint when a manifest exists but jq is missing" { init_with_remote create_project_dir needjq printf '{"version":2,"dotenv":[".env"]}\n' > .secrets.json local p; p=$(m_nojq_path) run env PATH="$p" "$SECRETS_BIN" push [ "$status" -eq 1 ] [[ "$output" == *"'jq' is not installed"* ]] || false } @test "help documents add, --frozen, --dry-run and the manifest" { run "$SECRETS_BIN" help [ "$status" -eq 0 ] [[ "$output" == *"secrets add"* ]] || false [[ "$output" == *"--frozen"* ]] || false [[ "$output" == *"--dry-run"* ]] || false [[ "$output" == *".secrets.json"* ]] || false } # ─── Coverage backfill (ship Step 7 gap paths) ───────────────────────── @test "json external entry with unknown type warns and is skipped" { init_with_remote local dir="$WORK_DIR/unktype"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf '{"version":2,"external":[{"type":"wat","path":"~/x.properties","keys":["k"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push unktype [ "$status" -eq 0 ] [[ "$output" == *"unknown external type"* ]] || false } @test "json external entry with unsafe path warns and is skipped" { init_with_remote local dir="$WORK_DIR/unsafext"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf '{"version":2,"external":[{"type":"properties","path":"~/../etc/x.properties","keys":["k"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push unsafext [ "$status" -eq 0 ] [[ "$output" == *"unsafe characters in external path"* ]] || false } @test "json external entry with unsafe keys warns and is skipped" { init_with_remote local dir="$WORK_DIR/unsafekeys"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf '{"version":2,"external":[{"type":"properties","path":"~/.gradle/gradle.properties","keys":["k;rm"]}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push unsafekeys [ "$status" -eq 0 ] [[ "$output" == *"unsafe characters in key list"* ]] || false } @test "json properties entry without keys warns and is skipped" { init_with_remote local dir="$WORK_DIR/nokeys"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf '{"version":2,"external":[{"type":"properties","path":"~/.gradle/gradle.properties"}]}\n' > "$dir/.secrets.json" cd "$dir" run "$SECRETS_BIN" push nokeys [ "$status" -eq 0 ] [[ "$output" == *"has no keys"* ]] || false } @test "symlinked .secrets-files is ignored with a warning on push" { init_with_remote m_gradle_src $'beaconClerkPkTest=x\n' local dir="$WORK_DIR/symlegacy"; mkdir -p "$dir" echo "K=v" > "$dir/.env" printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$TEST_TMPDIR/real-sf" ln -s "$TEST_TMPDIR/real-sf" "$dir/.secrets-files" cd "$dir" run "$SECRETS_BIN" push symlegacy [ "$status" -eq 0 ] [[ "$output" == *"symlink"* ]] || false run bash -c "ls $SECRETS_DIR/symlegacy/external/*.age 2>/dev/null" [ "$status" -ne 0 ] } @test "pull with declared entries dies with directed error when project absent from store" { init_with_remote mkdir -p "$WORK_DIR/ghostproj" cd "$WORK_DIR/ghostproj" printf '{"version":2,"dotenv":[".env"]}\n' > .secrets.json run "$SECRETS_BIN" pull ghostproj [ "$status" -eq 1 ] [[ "$output" == *"not found"* ]] || false [[ "$output" == *"secrets list"* ]] || false } @test "add without an argument dies with usage" { create_project_dir noargadd run "$SECRETS_BIN" add [ "$status" -eq 1 ] [[ "$output" == *"Usage: secrets add"* ]] || false } @test "push rejects an unknown flag with usage" { init_with_remote create_project_dir badflag run "$SECRETS_BIN" push --nope [ "$status" -eq 1 ] [[ "$output" == *"Unknown push flag"* ]] || false } @test "add normalizes a leading ./ prefix" { create_project_dir dotslash run "$SECRETS_BIN" add ./.env [ "$status" -eq 0 ] run jq -r '.dotenv[0]' .secrets.json [ "$output" = ".env" ] } @test "push --dry-run reports nothing-new when manifest covers all discovered files" { init_with_remote create_project_dir alldecl "$SECRETS_BIN" add .env >/dev/null "$SECRETS_BIN" add .env.staging >/dev/null run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"Nothing new to add"* ]] || false } @test "which displays external entries from the manifest" { create_project_dir whichext printf '{"version":2,"dotenv":[".env"],"external":[{"type":"properties","path":"~/.gradle/gradle.properties","keys":["k1"]}]}\n' > .secrets.json run "$SECRETS_BIN" which [ "$status" -eq 0 ] [[ "$output" == *"properties"* ]] || false [[ "$output" == *"gradle.properties"* ]] || false [[ "$output" == *"k1"* ]] || false }