chore: add branch readiness check
This commit is contained in:
parent
4463bf1fc7
commit
44660bf7e3
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 污染物检查。
|
||||
|
||||
@ -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` 重定向到 `/`。
|
||||
|
||||
@ -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",
|
||||
|
||||
104
scripts/check-branch-readiness.mjs
Normal file
104
scripts/check-branch-readiness.mjs
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user