From 06e85f8b0f69c16fc2edc4de971c041bbdd105af Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 May 2026 12:04:09 +0800 Subject: [PATCH] ci: add workflow hygiene gate --- README.md | 1 + docs/operations.md | 11 ++++++- docs/release-readiness.md | 2 +- package.json | 3 +- scripts/check-ci-hygiene.mjs | 59 ++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 scripts/check-ci-hygiene.mjs diff --git a/README.md b/README.md index ee1d2a52c..269e93c42 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/operations.md b/docs/operations.md index 2ff5dc249..f651d2a60 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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 diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 55b802cdf..54feb4b87 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -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`。 diff --git a/package.json b/package.json index 093922c91..e1575d225 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-ci-hygiene.mjs b/scripts/check-ci-hygiene.mjs new file mode 100644 index 000000000..6990a9781 --- /dev/null +++ b/scripts/check-ci-hygiene.mjs @@ -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.`);