ci: add workflow hygiene gate
This commit is contained in:
parent
8e9669c748
commit
06e85f8b0f
@ -71,6 +71,7 @@ pnpm dev
|
||||
pnpm lint
|
||||
pnpm check:logs
|
||||
pnpm check:brand
|
||||
pnpm check:ci
|
||||
pnpm check:artifacts
|
||||
pnpm check:quality
|
||||
pnpm clean:artifacts
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
pnpm lint
|
||||
pnpm check:logs
|
||||
pnpm check:brand
|
||||
pnpm check:ci
|
||||
pnpm exec tsc --noEmit --pretty false
|
||||
git diff --check
|
||||
pnpm check:artifacts
|
||||
@ -26,7 +27,7 @@ pnpm check:quality
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、类型检查、diff hygiene 和 tracked 污染物检查。
|
||||
`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、类型检查、diff hygiene 和 tracked 污染物检查。
|
||||
|
||||
CI 和本地提交前检查复用同一个入口,避免规则漂移。
|
||||
|
||||
@ -58,6 +59,14 @@ pnpm check:brand
|
||||
|
||||
`pnpm check:brand` 只扫描 README、核心 docs、GitHub 模板、workflow 和 package metadata 等公开交付入口。历史方案文档和兼容性 runtime key 不在本门禁范围内,避免为了改名破坏旧用户数据或研发记录。
|
||||
|
||||
## CI Hygiene
|
||||
|
||||
```bash
|
||||
pnpm check:ci
|
||||
```
|
||||
|
||||
`pnpm check:ci` 会检查 workflow 是否保留只读权限、concurrency、固定 Ubuntu runner、固定 pnpm 版本、frozen install、cache v4,并禁止 `@latest`、`ubuntu-latest`、`--no-lint` 或构建 fallback 这类不稳定入口回流。
|
||||
|
||||
## 首页性能烟测
|
||||
|
||||
```bash
|
||||
|
||||
@ -13,7 +13,7 @@ SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
|
||||
|
||||
预期:
|
||||
|
||||
- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、类型检查、diff hygiene 和 tracked 污染物检查。
|
||||
- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、类型检查、diff hygiene 和 tracked 污染物检查。
|
||||
- `pnpm smoke:routes` 验证 `/` 返回 200。
|
||||
- `/classic` 和 `/chat/test-id` 重定向到 `/`。
|
||||
- `/settings` 未登录重定向到 `/login?callbackUrl=%2Fsettings`。
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
"check:logs": "node scripts/check-log-hygiene.mjs && tsx scripts/check-log-redaction.ts",
|
||||
"check:artifacts": "node scripts/check-tracked-artifacts.mjs",
|
||||
"check:brand": "node scripts/check-brand-hygiene.mjs",
|
||||
"check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts",
|
||||
"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",
|
||||
"clean:artifacts": "node scripts/clean-local-artifacts.mjs",
|
||||
"smoke:routes": "node scripts/smoke-routes.mjs",
|
||||
"format": "pnpm exec ultracite fix",
|
||||
|
||||
59
scripts/check-ci-hygiene.mjs
Normal file
59
scripts/check-ci-hygiene.mjs
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const workflowFiles = [".github/workflows/lint.yml", ".github/workflows/playwright.yml"];
|
||||
const violations = [];
|
||||
|
||||
const assertIncludes = (file, source, expected, reason) => {
|
||||
if (!source.includes(expected)) {
|
||||
violations.push(`${file}: missing ${reason}: ${expected}`);
|
||||
}
|
||||
};
|
||||
|
||||
const assertNotMatches = (file, source, pattern, reason) => {
|
||||
const lines = source.split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
if (pattern.test(line)) {
|
||||
violations.push(`${file}:${index + 1}: ${reason}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for (const file of workflowFiles) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
|
||||
assertIncludes(file, source, "permissions:\n contents: read", "read-only workflow permissions");
|
||||
assertIncludes(file, source, "concurrency:", "workflow concurrency");
|
||||
assertIncludes(file, source, "cancel-in-progress: true", "stale run cancellation");
|
||||
assertIncludes(file, source, "runs-on: ubuntu-22.04", "pinned Ubuntu runner");
|
||||
assertIncludes(file, source, "pnpm/action-setup@v4", "pinned pnpm setup action major");
|
||||
assertIncludes(file, source, "version: 9.12.3", "pinned pnpm version");
|
||||
assertIncludes(file, source, "pnpm install --frozen-lockfile", "frozen dependency install");
|
||||
|
||||
assertNotMatches(file, source, /ubuntu-latest/, "floating runner");
|
||||
assertNotMatches(file, source, /actions\/cache@v3/, "outdated cache action");
|
||||
assertNotMatches(file, source, /@[Ll]atest\b/, "floating action or tool version");
|
||||
assertNotMatches(file, source, /--no-lint/, "build or test bypass flag");
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
|
||||
for (const [name, command] of Object.entries(packageJson.scripts ?? {})) {
|
||||
if (/@latest\b/.test(command)) {
|
||||
violations.push(`package.json scripts.${name}: floating tool version: ${command}`);
|
||||
}
|
||||
if (/npx\s+ultracite/.test(command)) {
|
||||
violations.push(`package.json scripts.${name}: use pnpm exec ultracite instead of npx: ${command}`);
|
||||
}
|
||||
if (/--no-lint|build:skip-prerender|Build fallback/.test(command)) {
|
||||
violations.push(`package.json scripts.${name}: build bypass is not allowed: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("CI hygiene check failed. Keep workflows pinned, least-privileged, and non-bypassing.");
|
||||
console.error(violations.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`CI hygiene check passed for ${workflowFiles.length} workflows.`);
|
||||
Loading…
x
Reference in New Issue
Block a user