patchrelay 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build-info.json
CHANGED
|
@@ -342,6 +342,21 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
342
342
|
&& issue.factoryState !== "awaiting_queue"
|
|
343
343
|
&& issue.activeRunId === undefined
|
|
344
344
|
&& ageMs >= RECONCILIATION_GRACE_MS) {
|
|
345
|
+
// Plan §6.1: when the PR is also approved, this is the
|
|
346
|
+
// "In Review · stuck at admission" condition — the lander would
|
|
347
|
+
// accept the verdict but branch CI is red and (post-§4.3) we no
|
|
348
|
+
// longer auto-repair. Keep the same scope/status pair so existing
|
|
349
|
+
// dashboards continue to surface it; just sharpen the message.
|
|
350
|
+
if (reviewDecision === "APPROVED") {
|
|
351
|
+
return {
|
|
352
|
+
ciEntry,
|
|
353
|
+
finding: {
|
|
354
|
+
status: "fail",
|
|
355
|
+
scope: "github:ci",
|
|
356
|
+
message: "In Review · stuck at admission — PR is approved but gate CI is red and no CI repair is running",
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
345
360
|
return {
|
|
346
361
|
ciEntry,
|
|
347
362
|
finding: {
|
package/dist/db/issue-store.js
CHANGED
|
@@ -402,6 +402,22 @@ export class IssueStore {
|
|
|
402
402
|
.all();
|
|
403
403
|
return rows.map(mapIssueRow);
|
|
404
404
|
}
|
|
405
|
+
// Issues that are approved by review-quill but stuck in In Review
|
|
406
|
+
// because branch CI is failing — the merge-steward never admits them.
|
|
407
|
+
// Plan §6.2: surface this as IN_REVIEW_STUCK so an operator notices
|
|
408
|
+
// before the issue goes silent for hours.
|
|
409
|
+
listApprovedRedCiIssues() {
|
|
410
|
+
const rows = this.connection
|
|
411
|
+
.prepare(`SELECT * FROM issues
|
|
412
|
+
WHERE factory_state = 'pr_open'
|
|
413
|
+
AND active_run_id IS NULL
|
|
414
|
+
AND pending_run_type IS NULL
|
|
415
|
+
AND pr_number IS NOT NULL
|
|
416
|
+
AND pr_review_state = 'approved'
|
|
417
|
+
AND pr_check_status = 'failure'`)
|
|
418
|
+
.all();
|
|
419
|
+
return rows.map(mapIssueRow);
|
|
420
|
+
}
|
|
405
421
|
replaceIssueDependencies(params) {
|
|
406
422
|
const now = isoNow();
|
|
407
423
|
this.connection
|
|
@@ -271,7 +271,7 @@ function buildStructuredReviewContext(context) {
|
|
|
271
271
|
lines.push("No inline review comments were captured for this review.");
|
|
272
272
|
return lines.join("\n");
|
|
273
273
|
}
|
|
274
|
-
lines.push(`Inline review comments captured: ${reviewComments.length}`, "Resolve each comment below or verify it is already fixed on the current head before you stop.", "
|
|
274
|
+
lines.push(`Inline review comments captured: ${reviewComments.length}`, "Resolve each comment below or verify it is already fixed on the current head before you stop.", "Complete the turn either by pushing a newer PR head with the fix, or — if your reviewer-pass produces only comments, test wording, or PR-body changes — by editing the PR body via `gh pr edit` instead of pushing. Do not push a commit that produces a patch-id-equivalent diff just to make the fix unmistakable.", "If you are blocked, deliberately escalate instead of pushing.", "");
|
|
275
275
|
for (const comment of reviewComments) {
|
|
276
276
|
const location = comment.path
|
|
277
277
|
? `${comment.path}${comment.line !== undefined ? `:${comment.line}` : ""}${comment.side ? ` (${comment.side})` : ""}`
|
|
@@ -293,12 +293,12 @@ function buildRequestedChangesContext(runType, context) {
|
|
|
293
293
|
const mode = resolveRequestedChangesMode(runType, context);
|
|
294
294
|
const lines = [];
|
|
295
295
|
if (mode === "branch_upkeep") {
|
|
296
|
-
lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch
|
|
296
|
+
lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch. Push a newer head only when the work actually changes the diff against the base; do not republish a patch-id-equivalent head.");
|
|
297
297
|
}
|
|
298
298
|
else {
|
|
299
299
|
const reviewer = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
|
|
300
300
|
const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
|
|
301
|
-
lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness
|
|
301
|
+
lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness on the current PR branch. Push a newer head only when the fix actually changes the diff; if the reviewer-pass produces only comments, test wording, or PR-body changes, edit the PR body via `gh pr edit` instead.", "Address the real concern behind the feedback and verify nearby invariants in the touched flow before you publish.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
|
|
302
302
|
appendStructuredReviewContext(lines, context);
|
|
303
303
|
}
|
|
304
304
|
return lines.join("\n").trim();
|
|
@@ -2,6 +2,11 @@ import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
|
2
2
|
import { execCommand } from "./utils.js";
|
|
3
3
|
const QUEUE_HEALTH_GRACE_MS = 120_000;
|
|
4
4
|
const QUEUE_HEALTH_PROBE_FAILURE_COOLDOWN_MS = 300_000;
|
|
5
|
+
// Plan §6.2: an approved PR with red branch CI for >= this long is
|
|
6
|
+
// stuck at admission — operator notice is needed before the issue
|
|
7
|
+
// goes silent for hours.
|
|
8
|
+
const IN_REVIEW_STUCK_THRESHOLD_MS = 30 * 60 * 1000;
|
|
9
|
+
const IN_REVIEW_STUCK_FEED_COOLDOWN_MS = 30 * 60 * 1000;
|
|
5
10
|
function isDuplicateProbe(issue, context) {
|
|
6
11
|
const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
7
12
|
const headSha = typeof context?.failureHeadSha === "string" ? context.failureHeadSha : undefined;
|
|
@@ -17,6 +22,7 @@ export class QueueHealthMonitor {
|
|
|
17
22
|
logger;
|
|
18
23
|
feed;
|
|
19
24
|
probeFailureFeedTimes = new Map();
|
|
25
|
+
inReviewStuckFeedTimes = new Map();
|
|
20
26
|
constructor(db, config, advancer, logger, feed) {
|
|
21
27
|
this.db = db;
|
|
22
28
|
this.config = config;
|
|
@@ -28,6 +34,42 @@ export class QueueHealthMonitor {
|
|
|
28
34
|
for (const issue of this.db.issues.listAwaitingQueueIssues()) {
|
|
29
35
|
await this.probeQueuedIssue(issue);
|
|
30
36
|
}
|
|
37
|
+
for (const issue of this.db.issues.listApprovedRedCiIssues()) {
|
|
38
|
+
this.probeInReviewStuckIssue(issue);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Plan §6.2: emit IN_REVIEW_STUCK when an approved PR has a red gate
|
|
42
|
+
// for more than 30 minutes. Consequence of plan §4.3 — branch CI
|
|
43
|
+
// failures while approved no longer trigger ci_repair, so the
|
|
44
|
+
// condition is otherwise invisible to the operator.
|
|
45
|
+
probeInReviewStuckIssue(issue) {
|
|
46
|
+
if (!issue.prNumber)
|
|
47
|
+
return;
|
|
48
|
+
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
49
|
+
if (!project)
|
|
50
|
+
return;
|
|
51
|
+
const reference = issue.lastGitHubFailureAt ?? issue.updatedAt;
|
|
52
|
+
const stuckMs = Date.now() - Date.parse(reference);
|
|
53
|
+
if (stuckMs < IN_REVIEW_STUCK_THRESHOLD_MS)
|
|
54
|
+
return;
|
|
55
|
+
const feedKey = `${issue.projectId}::${issue.linearIssueId}`;
|
|
56
|
+
const lastFedAt = this.inReviewStuckFeedTimes.get(feedKey) ?? 0;
|
|
57
|
+
if (Date.now() - lastFedAt < IN_REVIEW_STUCK_FEED_COOLDOWN_MS)
|
|
58
|
+
return;
|
|
59
|
+
this.inReviewStuckFeedTimes.set(feedKey, Date.now());
|
|
60
|
+
const minutes = Math.round(stuckMs / 60_000);
|
|
61
|
+
const failedCheck = issue.lastGitHubFailureCheckName ?? "branch CI";
|
|
62
|
+
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber, stuckMs, failedCheck }, "Queue health: approved PR is stuck at admission with red branch CI");
|
|
63
|
+
this.feed?.publish({
|
|
64
|
+
level: "warn",
|
|
65
|
+
kind: "github",
|
|
66
|
+
issueKey: issue.issueKey,
|
|
67
|
+
projectId: issue.projectId,
|
|
68
|
+
stage: "pr_open",
|
|
69
|
+
status: "in_review_stuck",
|
|
70
|
+
summary: `In Review · stuck at admission — PR #${issue.prNumber} has been approved with red ${failedCheck} for ${minutes} min`,
|
|
71
|
+
detail: issue.lastGitHubFailureCheckUrl ?? undefined,
|
|
72
|
+
});
|
|
31
73
|
}
|
|
32
74
|
async probeQueuedIssue(issue) {
|
|
33
75
|
if (!issue.prNumber)
|