From 26373d257bd6cb60d82299fedaee1be41803c1c7 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 20 Jul 2026 16:37:42 +0800 Subject: [PATCH] fix: reject compose env selector aliases --- deploy/validate-staging-env.sh | 4 +- .../specs/2026-07-20-staging-server-design.md | 2 +- frontend/tests/health-deployment.test.ts | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/deploy/validate-staging-env.sh b/deploy/validate-staging-env.sh index ddee7473..0c82f578 100755 --- a/deploy/validate-staging-env.sh +++ b/deploy/validate-staging-env.sh @@ -23,8 +23,10 @@ require_selector() { local key="$1" local expected="$2" local count + local definition_pattern - count="$(grep -c "^${key}=" "$ENV_FILE" || true)" + definition_pattern="^[[:space:]]*(export[[:space:]]+)?${key}([[:space:]]*=|[[:space:]]*$)" + count="$(grep -Ec "$definition_pattern" "$ENV_FILE" || true)" if [ "$count" -ne 1 ] || ! grep -Fqx "${key}=${expected}" "$ENV_FILE"; then echo "invalid staging selector: $key" >&2 exit 1 diff --git a/docs/superpowers/specs/2026-07-20-staging-server-design.md b/docs/superpowers/specs/2026-07-20-staging-server-design.md index 89d13b65..d2de9298 100644 --- a/docs/superpowers/specs/2026-07-20-staging-server-design.md +++ b/docs/superpowers/specs/2026-07-20-staging-server-design.md @@ -18,7 +18,7 @@ | 部署密钥 | production 专用 | staging 专用 | | 应用配置 | `.env.production` | `.env.staging` | | Supabase | 生产项目 | 独立 staging 项目 | -| 部署触发 | `main` CI 成功 | `staging` CI 成功或手动触发 | +| 部署触发 | 手动 production workflow | `staging` CI 成功或手动触发 | staging 不得写入生产数据库,不得复用 service-role key、数据库密码、SSH 私钥或模型计费密钥。模型接口优先使用独立测试 key、低额度或 provider sandbox。 diff --git a/frontend/tests/health-deployment.test.ts b/frontend/tests/health-deployment.test.ts index 4ecf9a0c..311eb4df 100644 --- a/frontend/tests/health-deployment.test.ts +++ b/frontend/tests/health-deployment.test.ts @@ -182,6 +182,7 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi ); const root = mkdtempSync(join(tmpdir(), "jyotisha-staging-env-")); const envFile = join(root, ".env.staging"); + const composeFile = join(root, "compose.yml"); const validSelectors = [ "APP_ENV_FILE=../.env.staging", "CADDYFILE_PATH=./Caddyfile.staging", @@ -195,6 +196,17 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi }; try { + writeFileSync( + composeFile, + [ + "services:", + " probe:", + " image: alpine", + " environment:", + " SELECTED: ${APP_ENV_FILE}", + "", + ].join("\n"), + ); writeEnv(validSelectors); assert.equal(run().status, 0); @@ -204,6 +216,34 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi writeEnv([...validSelectors, "SITE_ADDRESS=https://example.invalid"]); assert.notEqual(run().status, 0); + writeEnv([...validSelectors, "APP_ENV_FILE = ../.env.production"]); + assert.notEqual(run().status, 0); + const rendered = spawnSync( + "docker", + [ + "compose", + "--env-file", + envFile, + "-f", + composeFile, + "config", + "--format", + "json", + ], + { encoding: "utf8" }, + ); + assert.equal(rendered.status, 0, rendered.stderr); + assert.equal( + JSON.parse(rendered.stdout).services.probe.environment.SELECTED, + "../.env.production", + ); + + writeEnv([...validSelectors, "export CADDYFILE_PATH=./Caddyfile"]); + assert.notEqual(run().status, 0); + + writeEnv([...validSelectors, "SITE_ADDRESS"]); + assert.notEqual(run().status, 0); + writeEnv(validSelectors, 0o644); assert.notEqual(run().status, 0); } finally {