chore: add env example hygiene gate

This commit is contained in:
Your Name 2026-05-27 12:08:44 +08:00
parent 06e85f8b0f
commit 3c1fc15039
6 changed files with 191 additions and 3 deletions

View File

@ -11,6 +11,7 @@ AUTH_SECRET=replace-with-random-secret
# Public application URL. Required for production and self-hosted Auth.js.
AUTH_URL=https://your-domain.com
NEXTAUTH_URL=https://your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com
# Only enable behind a trusted reverse proxy with validated Host headers.
@ -81,6 +82,14 @@ FAL_BASE_URL=https://fal.run
FAL_MODEL=
FAL_REALTIME_MODEL=
# Realtime Canvas fallback providers.
REPLICATE_API_TOKEN=
BFL_API_KEY=
BFL_KEY=
BFL_BASE_URL=
BFL_MODEL=
RUNWARE_MODEL=
# 3D / CAD providers.
TRIPO_API_KEY=
TRIPO_BASE_URL=https://api.tripo3d.ai/v2/openapi
@ -96,6 +105,7 @@ ZOO_BASE_URL=https://api.zoo.dev
# RUXA / video callback.
RUXA_CALLBACK_URL=
RUXA_VIDEO_CALLBACK_URL=
# -----------------------------------------------------------------------------
# Product Planning Integrations
@ -111,6 +121,7 @@ TAOBAO_SALE_API_TOKEN=
PRODUCT_PLANNING_DB_HOST=
PRODUCT_PLANNING_DB_PORT=3306
PRODUCT_PLANNING_DB_DATABASE=
PRODUCT_PLANNING_DB_NAME=
PRODUCT_PLANNING_DB_USER=
PRODUCT_PLANNING_DB_PASSWORD=
@ -143,6 +154,9 @@ AGENT_PSD_REMOTE_ASSET_CACHE_MAX_ENTRY_BYTES=
AGENT_PSD_REMOTE_ASSET_CACHE_TTL_MS=
AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL=6
AGENT_PSD_RENDER_TASK_CONCURRENCY=2
NEXT_PUBLIC_AGENT_STYLE_PERF_MONITOR=true
NEXT_PUBLIC_AGENT_STYLE_PERF_SAMPLE_RATE=0.01
NEXT_PUBLIC_AGENT_STYLE_OPTIMIZED_RENDER=true
# -----------------------------------------------------------------------------
# Logging / Observability
@ -171,3 +185,6 @@ PORT=3001
PLAYWRIGHT=false
CI_PLAYWRIGHT=false
PLAYWRIGHT_TEST_BASE_URL=http://127.0.0.1:3001
SMOKE_BASE_URL=http://127.0.0.1:3001
SMOKE_TIMEOUT_MS=20000
DISABLE_PERF_GUARD_AUTO_INSTALL=false

View File

@ -72,6 +72,7 @@ pnpm lint
pnpm check:logs
pnpm check:brand
pnpm check:ci
pnpm check:env
pnpm check:artifacts
pnpm check:quality
pnpm clean:artifacts

View File

@ -9,6 +9,7 @@ pnpm lint
pnpm check:logs
pnpm check:brand
pnpm check:ci
pnpm check:env
pnpm exec tsc --noEmit --pretty false
git diff --check
pnpm check:artifacts
@ -27,7 +28,7 @@ pnpm check:quality
## CI
`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、类型检查、diff hygiene 和 tracked 污染物检查。
`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、环境变量示例卫生、类型检查、diff hygiene 和 tracked 污染物检查。
CI 和本地提交前检查复用同一个入口,避免规则漂移。
@ -67,6 +68,14 @@ pnpm check:ci
`pnpm check:ci` 会检查 workflow 是否保留只读权限、concurrency、固定 Ubuntu runner、固定 pnpm 版本、frozen install、cache v4,并禁止 `@latest`、`ubuntu-latest`、`--no-lint` 或构建 fallback 这类不稳定入口回流。
## 环境变量示例治理
```bash
pnpm check:env
```
`pnpm check:env` 会检查 `.env.example` 是否包含关键部署变量、是否存在重复 key、是否把 token/password/API key 这类字段填成疑似真实密钥。
## 首页性能烟测
```bash

View File

@ -13,7 +13,7 @@ SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
预期:
- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、类型检查、diff hygiene 和 tracked 污染物检查。
- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、环境变量示例卫生、类型检查、diff hygiene 和 tracked 污染物检查。
- `pnpm smoke:routes` 验证 `/` 返回 200。
- `/classic` 和 `/chat/test-id` 重定向到 `/`。
- `/settings` 未登录重定向到 `/login?callbackUrl=%2Fsettings`。

View File

@ -14,7 +14,8 @@
"check:artifacts": "node scripts/check-tracked-artifacts.mjs",
"check:brand": "node scripts/check-brand-hygiene.mjs",
"check:ci": "node scripts/check-ci-hygiene.mjs",
"check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm check:ci && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts",
"check:env": "node scripts/check-env-example.mjs",
"check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm check:ci && pnpm check:env && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts",
"clean:artifacts": "node scripts/clean-local-artifacts.mjs",
"smoke:routes": "node scripts/smoke-routes.mjs",
"format": "pnpm exec ultracite fix",

View File

@ -0,0 +1,160 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
const envExamplePath = ".env.example";
const source = readFileSync(envExamplePath, "utf8");
const lines = source.split(/\r?\n/);
const requiredKeys = [
"AUTH_SECRET",
"AUTH_URL",
"NEXTAUTH_URL",
"NEXT_PUBLIC_APP_URL",
"AUTH_TRUST_HOST",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"ADMIN_EMAIL",
"ADMIN_PASSWORD",
"RESEND_API_KEY",
"RESEND_FROM_EMAIL",
"EMAIL_VERIFICATION_SECRET",
"CRON_SECRET",
"POSTGRES_URL",
"POSTGRES_MAX_CONNECTIONS",
"POSTGRES_IDLE_TIMEOUT_SECONDS",
"POSTGRES_CONNECT_TIMEOUT_SECONDS",
"BLOB_READ_WRITE_TOKEN",
"S3_ENDPOINT",
"S3_REGION",
"S3_BUCKET",
"S3_ACCESS_KEY_ID",
"S3_SECRET_ACCESS_KEY",
"S3_PUBLIC_URL",
"AI_GATEWAY_API_KEY",
"NEXT_PUBLIC_DEFAULT_CHAT_MODEL",
"DASHSCOPE_BASE_URL",
"DASHSCOPE_API_KEY",
"GTP5_API_URL",
"GTP5_API_KEY",
"GTP5_MODEL",
"FAL_KEY",
"FAL_API_KEY",
"FAL_BASE_URL",
"FAL_MODEL",
"FAL_REALTIME_MODEL",
"REPLICATE_API_TOKEN",
"BFL_API_KEY",
"BFL_KEY",
"BFL_BASE_URL",
"BFL_MODEL",
"RUNWARE_MODEL",
"TRIPO_API_KEY",
"TRIPO_BASE_URL",
"WORLDLABS_API_KEY",
"WORLDLABS_BASE_URL",
"WORLDLABS_IMPORT_TOKEN",
"WORLDLABS_KEY_POOL_IMPORT_TOKEN",
"KITTYCAD_API_TOKEN",
"KITTYCAD_API_KEY",
"KITTYCAD_BASE_URL",
"ZOO_API_TOKEN",
"ZOO_BASE_URL",
"RUXA_CALLBACK_URL",
"RUXA_VIDEO_CALLBACK_URL",
"PRODUCT_PLANNING_DETAIL_PLUS_BASE_URL",
"PRODUCT_PLANNING_DETAIL_PLUS_INTERNAL_BASE_URL",
"PRODUCT_PLANNING_DETAIL_PLUS_API_KEY",
"PRODUCT_PLANNING_QA_TOKEN",
"TAOBAO_API_KEY",
"TAOBAO_API_SECRET",
"TAOBAO_SALE_API_TOKEN",
"PRODUCT_PLANNING_DB_HOST",
"PRODUCT_PLANNING_DB_PORT",
"PRODUCT_PLANNING_DB_DATABASE",
"PRODUCT_PLANNING_DB_NAME",
"PRODUCT_PLANNING_DB_USER",
"PRODUCT_PLANNING_DB_PASSWORD",
"PRODUCT_PLANNING_DB_CHARSET",
"PRODUCT_PLANNING_DB_SSL",
"CANVAS_IMAGE_TASK_CONCURRENCY",
"CANVAS_IMAGE_TASK_BATCH_DISPATCH_INTERVAL_MS",
"EXTERNAL_IMAGE_API_MAX_CONCURRENT",
"EXTERNAL_IMAGE_API_MAX_QUEUE",
"EXTERNAL_IMAGE_API_QUEUE_TIMEOUT_MS",
"AGENT_FORCE_LLM_PLANNER",
"AGENT_FORCE_QWEN3VL_FALLBACK",
"AGENT_IMAGE_GEN_LLM_OPTIMIZER_ENABLED",
"AGENT_LLM_PROMPT_OPTIMIZER_ENABLED",
"AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT",
"AGENT_PSD_LOCAL_CACHE_DIR",
"AGENT_PSD_REMOTE_ASSET_CACHE_DISABLED",
"AGENT_PSD_REMOTE_ASSET_CACHE_MAX_BYTES",
"AGENT_PSD_REMOTE_ASSET_CACHE_MAX_ENTRY_BYTES",
"AGENT_PSD_REMOTE_ASSET_CACHE_TTL_MS",
"AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL",
"AGENT_PSD_RENDER_TASK_CONCURRENCY",
"NEXT_PUBLIC_AGENT_STYLE_PERF_MONITOR",
"NEXT_PUBLIC_AGENT_STYLE_PERF_SAMPLE_RATE",
"NEXT_PUBLIC_AGENT_STYLE_OPTIMIZED_RENDER",
"LOG_LEVEL",
"LOG_INCLUDE_STACK",
"NEXT_PUBLIC_ENABLE_CLIENT_LOGS",
"VIDEO_ARCHIVE_DELAY_HOURS",
"PORT",
"PLAYWRIGHT",
"CI_PLAYWRIGHT",
"PLAYWRIGHT_TEST_BASE_URL",
"SMOKE_BASE_URL",
"SMOKE_TIMEOUT_MS",
"DISABLE_PERF_GUARD_AUTO_INSTALL",
];
const secretLikeKey = /(SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY|CLIENT_SECRET|FAL_KEY|BFL_KEY|ZOO_API_TOKEN|KITTYCAD_API_TOKEN)$/;
const suspiciousSecretValue =
/(sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{20,})/;
const allowedSecretPlaceholders = /^(|replace-with-[a-z0-9-]+|change-me[a-z0-9-]*|placeholder|example|your-[a-z0-9-]+)$/i;
const parsed = new Map();
const violations = [];
lines.forEach((line, index) => {
if (!line || line.trim().startsWith("#")) {
return;
}
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (!match) {
violations.push(`${envExamplePath}:${index + 1}: invalid env assignment: ${line}`);
return;
}
const [, key, rawValue] = match;
const value = rawValue.trim();
if (parsed.has(key)) {
violations.push(`${envExamplePath}:${index + 1}: duplicate key: ${key}`);
}
parsed.set(key, { value, line: index + 1 });
if (secretLikeKey.test(key) && value && !allowedSecretPlaceholders.test(value)) {
violations.push(`${envExamplePath}:${index + 1}: secret-like key must use an empty value or placeholder: ${key}`);
}
if (suspiciousSecretValue.test(value)) {
violations.push(`${envExamplePath}:${index + 1}: suspicious real secret value detected: ${key}`);
}
});
for (const key of requiredKeys) {
if (!parsed.has(key)) {
violations.push(`${envExamplePath}: missing documented env key: ${key}`);
}
}
if (violations.length > 0) {
console.error("Environment example hygiene check failed.");
console.error(violations.join("\n"));
process.exit(1);
}
console.log(`Environment example hygiene check passed for ${parsed.size} keys.`);