chore: add branch readiness check

This commit is contained in:
Your Name 2026-05-27 13:35:35 +08:00
parent 4463bf1fc7
commit 44660bf7e3
6 changed files with 129 additions and 0 deletions

View File

@ -13,6 +13,7 @@
## Required Verification
- [ ] `pnpm check:branch`
- [ ] `pnpm check:quality`
- [ ] `AUTH_TRUST_HOST=true PORT=3001 pnpm dev`
- [ ] `SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes`

View File

@ -72,6 +72,7 @@ pnpm lint
pnpm check:logs
pnpm check:brand
pnpm check:ci
pnpm check:branch
pnpm check:env
pnpm check:secrets
pnpm check:metadata
@ -91,6 +92,8 @@ pnpm db:generate
推荐提交前至少运行:
```bash
git fetch github main --prune
pnpm check:branch
pnpm check:quality
pnpm audit:security
AUTH_TRUST_HOST=true PORT=3001 pnpm dev

View File

@ -5,6 +5,8 @@
## 合并前检查
```bash
git fetch github main --prune
pnpm check:branch
pnpm lint
pnpm check:logs
pnpm check:brand
@ -40,6 +42,21 @@ SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm verify:release
`pnpm clean:artifacts` 会清理当前工作区常见本地垃圾文件,例如 `.DS_Store`、swap 文件和 `tsconfig.tsbuildinfo`。它会跳过 `.git`、`node_modules`、`.next`、`.pnpm-store`、`.venv` 等重目录,适合在提交或打包前手动执行。
## 分支与 PR 就绪检查
```bash
git fetch github main --prune
pnpm check:branch
```
`pnpm check:branch` 默认以 `github/main` 为基线,检查当前工作区是否干净、基线 ref 是否存在、当前分支相对 main 的 ahead/behind 状态、受影响文件数量,以及 dry-run merge tree 是否出现冲突标记。它会把 behind main 作为 warning,方便还没决定 rebase/merge 前先看风险;如果发布或合并策略要求必须最新,可使用:
```bash
REQUIRE_UP_TO_DATE=true pnpm check:branch
```
如果远端名不同,可以用 `PR_BASE_REF=origin/main pnpm check:branch` 或 `pnpm check:branch origin/main` 覆盖基线。提交前临时验证未提交改动时,可使用 `ALLOW_DIRTY=true pnpm check:branch` 或 `pnpm check:branch --allow-dirty`。
## CI
`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、环境变量示例卫生、secret hygiene、package metadata、测试门禁卫生、轻量 helper 回归测试、类型检查、diff hygiene 和 tracked 污染物检查。

View File

@ -6,6 +6,8 @@
```bash
pnpm install --frozen-lockfile
git fetch github main --prune
pnpm check:branch
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm verify:release
```
@ -13,6 +15,7 @@ SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm verify:release
预期:
- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、环境变量示例卫生、secret hygiene、package metadata、测试门禁卫生、轻量 helper 回归测试、类型检查、diff hygiene 和 tracked 污染物检查。
- `pnpm check:branch` 确认当前分支相对 `github/main` 没有 dry-run merge 冲突;behind main 会提示为 warning,发布前可用 `REQUIRE_UP_TO_DATE=true` 提升为失败。
- `pnpm audit:security` 输出已审计,Dependency Audit workflow 也应通过;若仍有 critical/high,必须在 [dependency-risk-register.md](dependency-risk-register.md) 中记录 remediation 或风险接受状态。
- `pnpm smoke:routes` 验证 `/` 返回 200。
- `/classic` 和 `/chat/test-id` 重定向到 `/`。

View File

@ -20,6 +20,7 @@
"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:branch": "node scripts/check-branch-readiness.mjs",
"check:env": "node scripts/check-env-example.mjs",
"check:secrets": "node scripts/check-secrets.mjs",
"check:metadata": "node scripts/check-package-metadata.mjs",

View File

@ -0,0 +1,104 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const args = process.argv.slice(2);
const baseRef = process.env.PR_BASE_REF || args.find((arg) => !arg.startsWith("--")) || "github/main";
const requireUpToDate =
process.env.REQUIRE_UP_TO_DATE === "true" || args.includes("--require-up-to-date");
const allowDirty = process.env.ALLOW_DIRTY === "true" || args.includes("--allow-dirty");
const failures = [];
const warnings = [];
const currentBranch = git(["branch", "--show-current"]).trim() || "HEAD";
const status = git(["status", "--porcelain"]);
if (status.trim() && !allowDirty) {
failures.push("working tree has uncommitted changes; commit, stash, or set ALLOW_DIRTY=true");
}
try {
git(["rev-parse", "--verify", baseRef]);
} catch {
failures.push(`base ref not found: ${baseRef}. Run \`git fetch\` or set PR_BASE_REF.`);
}
if (failures.length === 0) {
const mergeBase = git(["merge-base", "HEAD", baseRef]).trim();
const [behindText, aheadText] = git(["rev-list", "--left-right", "--count", `${baseRef}...HEAD`])
.trim()
.split(/\s+/);
const behind = Number.parseInt(behindText || "0", 10);
const ahead = Number.parseInt(aheadText || "0", 10);
const changedFiles = git(["diff", "--name-only", `${baseRef}...HEAD`])
.split(/\r?\n/)
.filter(Boolean).length;
const mergeTree = runMergeTree(mergeBase, baseRef);
const conflictLines = mergeTree
.split(/\r?\n/)
.filter((line) => /^(<<<<<<<|=======|>>>>>>>|CONFLICT\b)/.test(line));
if (conflictLines.length > 0) {
const conflictReportPath = join(tmpdir(), "yedir-branch-readiness-conflicts.txt");
writeFileSync(conflictReportPath, conflictLines.join("\n"));
failures.push(
`merge conflict markers detected against ${baseRef}; details: ${conflictReportPath}`
);
}
if (behind > 0) {
const message = `${currentBranch} is behind ${baseRef} by ${behind} commit(s)`;
if (requireUpToDate) {
failures.push(message);
} else {
warnings.push(message);
}
}
console.log(
JSON.stringify(
{
baseRef,
branch: currentBranch,
mergeBase,
ahead,
behind,
changedFiles,
mergeConflictMarkers: conflictLines.length,
requireUpToDate,
},
null,
2
)
);
}
if (warnings.length > 0) {
console.warn(`Branch readiness warnings:\n- ${warnings.join("\n- ")}`);
}
if (failures.length > 0) {
console.error(`Branch readiness failed:\n- ${failures.join("\n- ")}`);
process.exit(1);
}
console.log("Branch readiness check passed.");
function git(args) {
return execFileSync("git", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
}
function runMergeTree(mergeBase, baseRef) {
try {
return git(["merge-tree", mergeBase, "HEAD", baseRef]);
} catch (error) {
return [error.stdout, error.stderr].filter(Boolean).join("\n");
}
}