feat: 重构被审查分支的选择逻辑
push 审查 check_branches 现在只表示「要做 push 审查的分支」,不再兼作 PR 目标分支: - 勾选具体分支:这些分支的 push 会审查,通过后晋级到管理分支 - 新增「任意分支」(*):所有分支的 push 都审查 - 留空:只审 PR,不审 push PR 审查 始终覆盖「勾选的分支 + 管理分支」作为合并目标。 管理分支是最终目标,不需要出现在检查分支里也能被保护。 管理分支从候选中移除 分支选择器不再列出管理分支——它是终点,不需要再检查自己。 安全边界 「任意分支」只扩大审查范围,不会把任意分支自动合并进管理分支; 晋级仍要求分支被显式勾选(promotesToManaged)。 同时修正一处语义错误 早先 branchMatches 把空列表当成「审所有分支」,导致新建仓库在用户 还没选分支时就审查全部 push。现在空列表明确表示「只审 PR」。 测试:分支匹配矩阵重写,覆盖 push/PR/晋级/保护目标四类判定,共 20 项通过。
This commit is contained in:
+86
-26
@@ -64,26 +64,86 @@ export function parseRepoUrl(input) {
|
||||
return { owner, name, host };
|
||||
}
|
||||
|
||||
/** Whether `refName` is one of the branches this repository reviews. */
|
||||
/** Branch names are stored with or without the refs/heads/ prefix. */
|
||||
function shortRef(refName) {
|
||||
return String(refName || "").replace(/^refs\/heads\//, "");
|
||||
}
|
||||
|
||||
/** The "any branch" token that widens push review to every branch. */
|
||||
export const ANY_BRANCH = "*";
|
||||
|
||||
/**
|
||||
* Branches explicitly picked for checking.
|
||||
* The "any branch" token is not a branch name, so it is filtered out.
|
||||
*/
|
||||
export function selectedBranches(checkBranches) {
|
||||
return parseList(checkBranches).filter((b) => b !== ANY_BRANCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when pushes on every branch are reviewed.
|
||||
* An empty list means "pull requests only", not "every push" — newly added
|
||||
* repositories must not start reviewing pushes until branches are picked.
|
||||
*/
|
||||
export function reviewsAnyPush(checkBranches) {
|
||||
return parseList(checkBranches).includes(ANY_BRANCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a push to `refName` should be reviewed.
|
||||
* Selecting "any branch" reviews everything; otherwise only the listed ones.
|
||||
*/
|
||||
export function branchMatches(refName, checkBranches) {
|
||||
const list = parseList(checkBranches);
|
||||
if (list.length === 0) return true;
|
||||
const short = String(refName || "").replace(/^refs\/heads\//, "");
|
||||
return list.some((b) => b === short || b === refName);
|
||||
if (reviewsAnyPush(checkBranches)) return true;
|
||||
const name = shortRef(refName);
|
||||
return selectedBranches(checkBranches).includes(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a pull request should be reviewed.
|
||||
*
|
||||
* The check list names the *target* branches worth protecting, so a PR counts
|
||||
* when it merges INTO a checked branch. That keeps "review everything that
|
||||
* lands on prd/test" working even though feature branches are never listed.
|
||||
* As a fallback the head branch is also matched, so explicitly listing a
|
||||
* long-lived branch still reviews pushes and PRs originating from it.
|
||||
* A pull request counts when it merges INTO a checked branch, or into the
|
||||
* managed branch — the managed branch is the final target, so it is protected
|
||||
* without needing to be listed as a checked branch. Matching the head branch
|
||||
* is kept as a fallback so explicitly listing a long-lived branch still covers
|
||||
* pull requests originating from it.
|
||||
*/
|
||||
export function pullRequestMatches(headRef, baseRef, checkBranches) {
|
||||
if (branchMatches(baseRef, checkBranches)) return true;
|
||||
return branchMatches(headRef, checkBranches);
|
||||
export function pullRequestMatches(headRef, baseRef, checkBranches, managedBranch) {
|
||||
const target = shortRef(baseRef);
|
||||
const head = shortRef(headRef);
|
||||
const list = selectedBranches(checkBranches);
|
||||
|
||||
if (managedBranch) {
|
||||
// The managed branch is the final target: always protected for pull
|
||||
// requests, even though it is deliberately absent from the checked list.
|
||||
if (target === shortRef(managedBranch)) return true;
|
||||
return list.includes(target) || list.includes(head);
|
||||
}
|
||||
// No managed branch configured: an empty list falls back to reviewing all
|
||||
// pull requests rather than silently reviewing none.
|
||||
if (list.length === 0) return true;
|
||||
return list.includes(target) || list.includes(head);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a push to this branch should be promoted into the managed branch.
|
||||
*
|
||||
* Only explicitly listed branches promote. Selecting "any branch" widens
|
||||
* *review* to every branch but must not auto-merge arbitrary branches into the
|
||||
* managed branch, so it never promotes on its own.
|
||||
*/
|
||||
export function promotesToManaged(refName, checkBranches, managedBranch) {
|
||||
const name = shortRef(refName);
|
||||
if (!name || name === shortRef(managedBranch)) return false;
|
||||
return selectedBranches(checkBranches).includes(name);
|
||||
}
|
||||
|
||||
/** Whether `baseRef` is a branch that pull requests may be merged into. */
|
||||
export function isProtectedTarget(baseRef, checkBranches, managedBranch) {
|
||||
const target = shortRef(baseRef);
|
||||
if (!target) return false;
|
||||
if (managedBranch && target === shortRef(managedBranch)) return true;
|
||||
return selectedBranches(checkBranches).includes(target);
|
||||
}
|
||||
|
||||
function badge(comment) {
|
||||
@@ -714,8 +774,8 @@ export class ReviewEngine {
|
||||
// A two-stage flow (feature -> test -> prd) legitimately merges into a
|
||||
// checked branch that is not the managed branch, so the gate is "is the
|
||||
// target one of the branches we protect", not "is it the managed branch".
|
||||
if (job.base_ref && !branchMatches(job.base_ref, repo.check_branches)) {
|
||||
appendLog(`auto-merge skipped: PR targets ${job.base_ref}, which is not in check_branches (${repo.check_branches})`);
|
||||
if (job.base_ref && !isProtectedTarget(job.base_ref, repo.check_branches, repo.managed_branch)) {
|
||||
appendLog(`auto-merge skipped: PR targets ${job.base_ref}, which is neither the managed branch nor a checked branch`);
|
||||
return false;
|
||||
}
|
||||
if (reviewIncomplete) {
|
||||
@@ -798,21 +858,21 @@ export class ReviewEngine {
|
||||
appendLog(`auto-merge skipped: commit status is ${statusState}`);
|
||||
return false;
|
||||
}
|
||||
if (!branchMatches(job.ref_name, repo.check_branches)) {
|
||||
appendLog(`auto-merge skipped: ${job.ref_name} is not a checked branch`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk up the chain: test -> prd. The managed branch is the top, so a push
|
||||
// to it has nowhere to go.
|
||||
const managed = repo.managed_branch;
|
||||
if (job.ref_name === managed) {
|
||||
appendLog(`auto-merge skipped: ${job.ref_name} is the managed branch`);
|
||||
// Only explicitly listed branches promote. "任意分支" widens review to every
|
||||
// branch but must not auto-merge arbitrary branches into the managed one.
|
||||
if (!promotesToManaged(job.ref_name, repo.check_branches, managed)) {
|
||||
if (reviewsAnyPush(repo.check_branches)) {
|
||||
appendLog(`auto-merge skipped: ${job.ref_name} is not an explicitly checked branch`);
|
||||
} else {
|
||||
appendLog(`auto-merge skipped: ${job.ref_name} is not a checked branch`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// The managed branch is the top of the chain and has nowhere to promote to.
|
||||
const target = managed;
|
||||
if (!branchMatches(target, repo.check_branches) && target !== managed) {
|
||||
appendLog(`auto-merge skipped: ${target} is not a checked branch`);
|
||||
if (!target) {
|
||||
appendLog("auto-merge skipped: no managed branch configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user