patchrelay 0.61.0 → 0.63.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
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export const DEFAULT_MERGE_QUEUE_LABEL = "queue";
|
|
2
2
|
export const DEFAULT_MERGE_QUEUE_CHECK_NAME = "merge-steward/queue";
|
|
3
3
|
export const DEFAULT_PRIORITY_QUEUE_LABEL = "queue:priority";
|
|
4
|
+
// Plan §2.4 defaults. Wired here so the resolver is the single source
|
|
5
|
+
// of truth for bus-contract values; consumers (review-quill subscribe,
|
|
6
|
+
// Linear-status sync, etc.) never read `project.github.*` directly.
|
|
7
|
+
export const DEFAULT_SPEC_READY_CHECK_NAME = "merge-steward/spec-ready";
|
|
8
|
+
export const DEFAULT_SPEC_BRANCH_PATTERN = "mq-spec-*";
|
|
9
|
+
export const DEFAULT_NO_CACHE_LABEL = "review:no-cache";
|
|
10
|
+
export const DEFAULT_QUEUED_FOR_DEPLOY_LABEL = "queued-for-deploy";
|
|
4
11
|
export function resolveMergeQueueProtocol(project) {
|
|
5
12
|
return {
|
|
6
13
|
repoFullName: project?.github?.repoFullName,
|
|
@@ -8,5 +15,9 @@ export function resolveMergeQueueProtocol(project) {
|
|
|
8
15
|
admissionLabel: project?.github?.mergeQueueLabel ?? DEFAULT_MERGE_QUEUE_LABEL,
|
|
9
16
|
evictionCheckName: project?.github?.mergeQueueCheckName ?? DEFAULT_MERGE_QUEUE_CHECK_NAME,
|
|
10
17
|
priorityLabel: project?.github?.priorityQueueLabel ?? DEFAULT_PRIORITY_QUEUE_LABEL,
|
|
18
|
+
specReadyCheckName: project?.github?.specReadyCheckName ?? DEFAULT_SPEC_READY_CHECK_NAME,
|
|
19
|
+
specBranchPattern: project?.github?.specBranchPattern ?? DEFAULT_SPEC_BRANCH_PATTERN,
|
|
20
|
+
noCacheLabel: project?.github?.noCacheLabel ?? DEFAULT_NO_CACHE_LABEL,
|
|
21
|
+
queuedForDeployLabel: project?.github?.queuedForDeployLabel ?? DEFAULT_QUEUED_FOR_DEPLOY_LABEL,
|
|
11
22
|
};
|
|
12
23
|
}
|
|
@@ -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)
|