fix: 所有问题都建 Issue、合并失败重试、push 与 PR 事件分流

1. 所有问题都建 Issue(原先只有阻断级才建)
   建 Issue 的判据从 blocking 改为 findings:任何等级的问题都创建/更新 Issue,
   等级体现在标题 [OCR][medium] 与标签 medium 上,阻断项在正文标注
   「(阻断合并)」。低等级问题不再丢失,也不会挡住合并。
   无任何发现时才关闭该分支的 Issue。

2. 合并失败自动重试
   Gitea 在算完 PR 可合并性之前会返回 405 Please try again later,
   原先直接放弃,晋级随机失败。现在对 405/409/5xx 按指数退避重试 4 次;
   权限不足、真实冲突等永久失败立即放弃并往 PR 留言说明。
   PR 已被合并(405 already merged)视为成功,幂等收尾。

3. push 与 PR 事件分流(本次新发现的 bug)
   合并路径原先按「是否找到关联 PR」判断,于是一个残留的 test→prd PR
   会让后续 push 被当成 PR 事件,走错分支并跳过晋级,日志还会给出
   「PR targets prd, which is not a checked branch」这种与实际不符的原因。
   现在按事件类型决定:trigger 以 pull_request 开头才走合并 PR 路径,
   push/manual 一律走晋级分支。关联 PR 仅用于评论归属。

验证:
- 直接 push test → 建出 issue #4([OCR][medium],标签 code-review,medium)
- 合并重试与幂等分支的判定表全部通过
- 事件分流判定表:push/manual → promote,pull_request.* → merge PR
This commit is contained in:
2026-09-21 15:16:06 +08:00
parent a0b15fb2d9
commit 7cd2c85888
4 changed files with 162 additions and 31 deletions
+38
View File
@@ -345,6 +345,44 @@ export class GiteaClient {
}
}
/**
* Gitea answers 405 "Please try again later" while it is still computing a pull
* request's mergeability, and 409 for transient lock contention. Both clear up
* on their own, so a merge is retried on those before giving up.
*/
export function isTransientMergeError(err) {
if (!err) return false;
const msg = String(err.message || "");
// Already merged is a final state, not something to retry.
if (/already merged/i.test(msg)) return false;
if (err.status === 405 || err.status === 409) return true;
if (err.status === 500 || err.status === 502 || err.status === 503 || err.status === 504) return true;
return /try again later|please try again|mergeability|not ready/i.test(msg);
}
/** True when a merge failed only because the pull request is already merged. */
export function isAlreadyMerged(err) {
return /already merged/i.test(String(err?.message || ""));
}
/** Retry `fn` while it throws a transient error, with exponential backoff. */
export async function retryTransient(fn, { attempts = 4, baseDelayMs = 1500, onRetry } = {}) {
let delay = baseDelayMs;
let lastErr;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (!isTransientMergeError(err) || attempt === attempts) throw err;
onRetry?.(attempt, err);
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 2, 12000);
}
}
throw lastErr;
}
/** Derive the repository web URL from an API base URL. */
export function webBaseUrl(apiBaseUrl) {
return normalizeBaseUrl(apiBaseUrl);