chore: add env example hygiene gate
This commit is contained in:
parent
06e85f8b0f
commit
3c1fc15039
17
.env.example
17
.env.example
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`。
|
||||
|
||||
@ -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",
|
||||
|
||||
160
scripts/check-env-example.mjs
Normal file
160
scripts/check-env-example.mjs
Normal 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.`);
|
||||
Loading…
x
Reference in New Issue
Block a user