patchrelay 0.68.3 → 0.68.5
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 +3 -3
- package/dist/cli/cluster-health/active-overlap.js +64 -0
- package/dist/cli/cluster-health/ci-classification.js +186 -0
- package/dist/cli/cluster-health/github-issue-health.js +212 -0
- package/dist/cli/cluster-health/github-probe.js +67 -0
- package/dist/cli/cluster-health/index.js +168 -0
- package/dist/cli/cluster-health/local-issue-health.js +91 -0
- package/dist/cli/cluster-health/review-quill-probe.js +126 -0
- package/dist/cli/cluster-health/service-probe.js +65 -0
- package/dist/cli/cluster-health/shared.js +13 -0
- package/dist/cli/cluster-health/types.js +1 -0
- package/dist/cli/commands/cluster.js +1 -1
- package/dist/codex-app-server.js +46 -9
- package/dist/idle-reconciliation.js +16 -36
- package/dist/reconcile-pr-fetch.js +23 -0
- package/dist/reconcile-pr-state-updates.js +40 -0
- package/dist/run-orchestrator.js +1 -1
- package/dist/run-reconciler.js +57 -1
- package/package.json +1 -1
- package/dist/cli/cluster-health.js +0 -976
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function normalizedReviewDecision(value) {
|
|
2
|
+
return value?.trim().toUpperCase();
|
|
3
|
+
}
|
|
4
|
+
function reviewDecisionToPrReviewState(value) {
|
|
5
|
+
const normalized = normalizedReviewDecision(value);
|
|
6
|
+
if (normalized === "APPROVED")
|
|
7
|
+
return "approved";
|
|
8
|
+
if (normalized === "CHANGES_REQUESTED")
|
|
9
|
+
return "changes_requested";
|
|
10
|
+
if (normalized === "REVIEW_REQUIRED")
|
|
11
|
+
return "commented";
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Pure projection from a fresh `gh pr view` snapshot into the issue-row
|
|
16
|
+
* fields that should be written back. Keeps the snapshot row in sync with
|
|
17
|
+
* GitHub truth and records a CI snapshot row (for the queue-health monitor
|
|
18
|
+
* and the operator feed) when both the head SHA and a gate check status
|
|
19
|
+
* are observable.
|
|
20
|
+
*
|
|
21
|
+
* Settled-at is null when the gate check is still pending so callers can
|
|
22
|
+
* tell a freshly-observed "in progress" from a recently-settled result.
|
|
23
|
+
*/
|
|
24
|
+
export function buildPrStateUpdates(pr, gateCheckStatus, primaryGateCheckName, now = () => new Date()) {
|
|
25
|
+
const prReviewState = reviewDecisionToPrReviewState(pr.reviewDecision);
|
|
26
|
+
return {
|
|
27
|
+
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
28
|
+
...(pr.state === "OPEN" ? { prState: "open" } : {}),
|
|
29
|
+
...(prReviewState ? { prReviewState } : {}),
|
|
30
|
+
...(gateCheckStatus ? { prCheckStatus: gateCheckStatus } : {}),
|
|
31
|
+
...(pr.headRefOid && gateCheckStatus
|
|
32
|
+
? {
|
|
33
|
+
lastGitHubCiSnapshotHeadSha: pr.headRefOid,
|
|
34
|
+
lastGitHubCiSnapshotGateCheckName: primaryGateCheckName,
|
|
35
|
+
lastGitHubCiSnapshotGateCheckStatus: gateCheckStatus,
|
|
36
|
+
lastGitHubCiSnapshotSettledAt: gateCheckStatus === "pending" ? null : now().toISOString(),
|
|
37
|
+
}
|
|
38
|
+
: {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -134,7 +134,7 @@ export class RunOrchestrator {
|
|
|
134
134
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed);
|
|
135
135
|
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.getHeldLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.leasePorts.releaseLease, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
136
136
|
this.interruptedRunRecovery = new InterruptedRunRecovery(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.recoveryPorts.failRunAndClear, this.recoveryPorts.restoreIdleWorktree, this.runCompletionPolicy, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
137
|
-
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, feed);
|
|
137
|
+
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, (projectId) => this.config.projects.find((project) => project.id === projectId)?.github?.repoFullName, feed);
|
|
138
138
|
this.runWakePlanner = new RunWakePlanner(db);
|
|
139
139
|
this.idleReconciler = new IdleIssueReconciler(db, config, this.wakeDispatcher, logger, feed);
|
|
140
140
|
this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, this.wakeDispatcher, logger, feed);
|
package/dist/run-reconciler.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getThreadTurns } from "./codex-thread-utils.js";
|
|
|
6
6
|
import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
|
|
7
7
|
import { resolveEffectiveActiveRun } from "./effective-active-run.js";
|
|
8
8
|
import { isThreadMaterializingError } from "./codex-thread-errors.js";
|
|
9
|
+
import { fetchPullRequestSnapshot } from "./reconcile-pr-fetch.js";
|
|
9
10
|
const THREAD_MATERIALIZATION_GRACE_MS = 10 * 60_000;
|
|
10
11
|
function isWithinThreadMaterializationGrace(run, nowMs = Date.now()) {
|
|
11
12
|
const startedAtMs = Date.parse(run.startedAt);
|
|
@@ -24,8 +25,9 @@ export class RunReconciler {
|
|
|
24
25
|
releaseLease;
|
|
25
26
|
readThreadWithRetry;
|
|
26
27
|
recoverOrEscalate;
|
|
28
|
+
resolveRepoFullName;
|
|
27
29
|
feed;
|
|
28
|
-
constructor(db, logger, linearProvider, linearSync, interruptedRunRecovery, runFinalizer, withHeldLease, releaseLease, readThreadWithRetry, recoverOrEscalate, feed) {
|
|
30
|
+
constructor(db, logger, linearProvider, linearSync, interruptedRunRecovery, runFinalizer, withHeldLease, releaseLease, readThreadWithRetry, recoverOrEscalate, resolveRepoFullName = () => undefined, feed) {
|
|
29
31
|
this.db = db;
|
|
30
32
|
this.logger = logger;
|
|
31
33
|
this.linearProvider = linearProvider;
|
|
@@ -36,6 +38,7 @@ export class RunReconciler {
|
|
|
36
38
|
this.releaseLease = releaseLease;
|
|
37
39
|
this.readThreadWithRetry = readThreadWithRetry;
|
|
38
40
|
this.recoverOrEscalate = recoverOrEscalate;
|
|
41
|
+
this.resolveRepoFullName = resolveRepoFullName;
|
|
39
42
|
this.feed = feed;
|
|
40
43
|
}
|
|
41
44
|
async reconcile(params) {
|
|
@@ -76,6 +79,9 @@ export class RunReconciler {
|
|
|
76
79
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
82
|
+
if (await this.releaseRunIfPullRequestMerged(run, effectiveIssue)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
79
85
|
if (!run.threadId) {
|
|
80
86
|
if (recoveryLease === "owned") {
|
|
81
87
|
this.logger.debug({ issueKey: effectiveIssue.issueKey, runId: run.id, runType: run.runType }, "Skipping zombie reconciliation for locally-owned launch that has not created a thread yet");
|
|
@@ -171,6 +177,56 @@ export class RunReconciler {
|
|
|
171
177
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
180
|
+
async releaseRunIfPullRequestMerged(run, issue) {
|
|
181
|
+
if (issue.prNumber === undefined)
|
|
182
|
+
return false;
|
|
183
|
+
if (issue.prState === "merged") {
|
|
184
|
+
this.releaseMergedRun(run, issue, "Cached PR state is merged");
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const repoFullName = this.resolveRepoFullName(issue.projectId);
|
|
188
|
+
if (!repoFullName)
|
|
189
|
+
return false;
|
|
190
|
+
const snapshot = await fetchPullRequestSnapshot(repoFullName, issue.prNumber);
|
|
191
|
+
if (!snapshot.ok) {
|
|
192
|
+
this.logger.debug({ issueKey: issue.issueKey, prNumber: issue.prNumber, error: snapshot.error.message }, "Could not refresh active-run PR state during reconciliation");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (snapshot.pr.state !== "MERGED")
|
|
196
|
+
return false;
|
|
197
|
+
this.releaseMergedRun(run, issue, "Pull request merged while the active Codex run was still marked running");
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
releaseMergedRun(run, issue, reason) {
|
|
201
|
+
this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
202
|
+
this.db.issueSessions.clearPendingIssueSessionEvents(run.projectId, run.linearIssueId);
|
|
203
|
+
this.db.runs.finishRun(run.id, {
|
|
204
|
+
status: "released",
|
|
205
|
+
failureReason: reason,
|
|
206
|
+
});
|
|
207
|
+
this.db.issues.upsertIssue({
|
|
208
|
+
projectId: run.projectId,
|
|
209
|
+
linearIssueId: run.linearIssueId,
|
|
210
|
+
activeRunId: null,
|
|
211
|
+
factoryState: "done",
|
|
212
|
+
prState: "merged",
|
|
213
|
+
pendingRunType: null,
|
|
214
|
+
pendingRunContextJson: null,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
this.feed?.publish({
|
|
218
|
+
level: "info",
|
|
219
|
+
kind: "stage",
|
|
220
|
+
issueKey: issue.issueKey,
|
|
221
|
+
projectId: run.projectId,
|
|
222
|
+
stage: "done",
|
|
223
|
+
status: "reconciled",
|
|
224
|
+
summary: `Released active ${run.runType} run after PR merge`,
|
|
225
|
+
});
|
|
226
|
+
const doneIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
227
|
+
void this.linearSync.syncSession(doneIssue, { activeRunType: run.runType });
|
|
228
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
229
|
+
}
|
|
174
230
|
async confirmDelegationAuthorityBeforeRelease(run, issue) {
|
|
175
231
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(run.projectId);
|
|
176
232
|
const linear = await this.linearProvider.forProject(run.projectId).catch(() => undefined);
|