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.
|
# Public application URL. Required for production and self-hosted Auth.js.
|
||||||
AUTH_URL=https://your-domain.com
|
AUTH_URL=https://your-domain.com
|
||||||
|
NEXTAUTH_URL=https://your-domain.com
|
||||||
NEXT_PUBLIC_APP_URL=https://your-domain.com
|
NEXT_PUBLIC_APP_URL=https://your-domain.com
|
||||||
|
|
||||||
# Only enable behind a trusted reverse proxy with validated Host headers.
|
# Only enable behind a trusted reverse proxy with validated Host headers.
|
||||||
@ -81,6 +82,14 @@ FAL_BASE_URL=https://fal.run
|
|||||||
FAL_MODEL=
|
FAL_MODEL=
|
||||||
FAL_REALTIME_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.
|
# 3D / CAD providers.
|
||||||
TRIPO_API_KEY=
|
TRIPO_API_KEY=
|
||||||
TRIPO_BASE_URL=https://api.tripo3d.ai/v2/openapi
|
TRIPO_BASE_URL=https://api.tripo3d.ai/v2/openapi
|
||||||
@ -96,6 +105,7 @@ ZOO_BASE_URL=https://api.zoo.dev
|
|||||||
|
|
||||||
# RUXA / video callback.
|
# RUXA / video callback.
|
||||||
RUXA_CALLBACK_URL=
|
RUXA_CALLBACK_URL=
|
||||||
|
RUXA_VIDEO_CALLBACK_URL=
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Product Planning Integrations
|
# Product Planning Integrations
|
||||||
@ -111,6 +121,7 @@ TAOBAO_SALE_API_TOKEN=
|
|||||||
|
|
||||||
PRODUCT_PLANNING_DB_HOST=
|
PRODUCT_PLANNING_DB_HOST=
|
||||||
PRODUCT_PLANNING_DB_PORT=3306
|
PRODUCT_PLANNING_DB_PORT=3306
|
||||||
|
PRODUCT_PLANNING_DB_DATABASE=
|
||||||
PRODUCT_PLANNING_DB_NAME=
|
PRODUCT_PLANNING_DB_NAME=
|
||||||
PRODUCT_PLANNING_DB_USER=
|
PRODUCT_PLANNING_DB_USER=
|
||||||
PRODUCT_PLANNING_DB_PASSWORD=
|
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_REMOTE_ASSET_CACHE_TTL_MS=
|
||||||
AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL=6
|
AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL=6
|
||||||
AGENT_PSD_RENDER_TASK_CONCURRENCY=2
|
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
|
# Logging / Observability
|
||||||
@ -171,3 +185,6 @@ PORT=3001
|
|||||||
PLAYWRIGHT=false
|
PLAYWRIGHT=false
|
||||||
CI_PLAYWRIGHT=false
|
CI_PLAYWRIGHT=false
|
||||||
PLAYWRIGHT_TEST_BASE_URL=http://127.0.0.1:3001
|
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:logs
|
||||||
pnpm check:brand
|
pnpm check:brand
|
||||||
pnpm check:ci
|
pnpm check:ci
|
||||||
|
pnpm check:env
|
||||||
pnpm check:artifacts
|
pnpm check:artifacts
|
||||||
pnpm check:quality
|
pnpm check:quality
|
||||||
pnpm clean:artifacts
|
pnpm clean:artifacts
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pnpm lint
|
|||||||
pnpm check:logs
|
pnpm check:logs
|
||||||
pnpm check:brand
|
pnpm check:brand
|
||||||
pnpm check:ci
|
pnpm check:ci
|
||||||
|
pnpm check:env
|
||||||
pnpm exec tsc --noEmit --pretty false
|
pnpm exec tsc --noEmit --pretty false
|
||||||
git diff --check
|
git diff --check
|
||||||
pnpm check:artifacts
|
pnpm check:artifacts
|
||||||
@ -27,7 +28,7 @@ pnpm check:quality
|
|||||||
|
|
||||||
## CI
|
## 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 和本地提交前检查复用同一个入口,避免规则漂移。
|
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 这类不稳定入口回流。
|
`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
|
```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。
|
- `pnpm smoke:routes` 验证 `/` 返回 200。
|
||||||
- `/classic` 和 `/chat/test-id` 重定向到 `/`。
|
- `/classic` 和 `/chat/test-id` 重定向到 `/`。
|
||||||
- `/settings` 未登录重定向到 `/login?callbackUrl=%2Fsettings`。
|
- `/settings` 未登录重定向到 `/login?callbackUrl=%2Fsettings`。
|
||||||
|
|||||||
@ -14,7 +14,8 @@
|
|||||||
"check:artifacts": "node scripts/check-tracked-artifacts.mjs",
|
"check:artifacts": "node scripts/check-tracked-artifacts.mjs",
|
||||||
"check:brand": "node scripts/check-brand-hygiene.mjs",
|
"check:brand": "node scripts/check-brand-hygiene.mjs",
|
||||||
"check:ci": "node scripts/check-ci-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",
|
"clean:artifacts": "node scripts/clean-local-artifacts.mjs",
|
||||||
"smoke:routes": "node scripts/smoke-routes.mjs",
|
"smoke:routes": "node scripts/smoke-routes.mjs",
|
||||||
"format": "pnpm exec ultracite fix",
|
"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