patchrelay 0.72.0 → 0.73.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/agent-session-plan.js +7 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/state-visualization.js +8 -1
- package/dist/db/issue-store.js +3 -0
- package/dist/db/issue-upsert-columns.js +1 -0
- package/dist/db/migrations.js +4 -0
- package/dist/github-webhook-terminal-handler.js +14 -2
- package/dist/idle-reconciliation.js +103 -3
- package/dist/linear-status-comment-sync.js +2 -0
- package/dist/linear-workflow-state-sync.js +3 -4
- package/dist/merge-queue-protocol.js +1 -0
- package/dist/post-merge-deploy.js +83 -0
- package/package.json +1 -1
|
@@ -161,6 +161,13 @@ export function buildAgentSessionPlan(params) {
|
|
|
161
161
|
], ["completed", "completed", "completed", "inProgress"]);
|
|
162
162
|
case "repairing_queue":
|
|
163
163
|
return setStatuses(queueRepairPlan(params.queueRepairAttempts ?? 1), ["completed", "completed", "completed", "inProgress"]);
|
|
164
|
+
case "deploying":
|
|
165
|
+
return setStatuses([
|
|
166
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
167
|
+
{ content: "Verification passed", status: "completed" },
|
|
168
|
+
{ content: "Merged", status: "completed" },
|
|
169
|
+
{ content: "Deploying", status: "inProgress" },
|
|
170
|
+
], ["completed", "completed", "completed", "inProgress"]);
|
|
164
171
|
case "awaiting_input":
|
|
165
172
|
return awaitingInputPlan();
|
|
166
173
|
case "escalated":
|
package/dist/build-info.json
CHANGED
|
@@ -8,12 +8,13 @@ const STATE_LABELS = {
|
|
|
8
8
|
repairing_ci: "repairing_ci",
|
|
9
9
|
awaiting_queue: "awaiting_queue",
|
|
10
10
|
repairing_queue: "repairing_queue",
|
|
11
|
+
deploying: "deploying",
|
|
11
12
|
awaiting_input: "awaiting_input",
|
|
12
13
|
escalated: "escalated",
|
|
13
14
|
done: "done",
|
|
14
15
|
failed: "failed",
|
|
15
16
|
};
|
|
16
|
-
const MAIN_STATES = ["delegated", "implementing", "pr_open", "awaiting_queue", "done"];
|
|
17
|
+
const MAIN_STATES = ["delegated", "implementing", "pr_open", "awaiting_queue", "deploying", "done"];
|
|
17
18
|
const PR_LOOP_STATES = ["changes_requested", "repairing_ci"];
|
|
18
19
|
const QUEUE_LOOP_STATES = ["repairing_queue"];
|
|
19
20
|
const EXIT_STATES = ["awaiting_input", "escalated", "failed"];
|
|
@@ -139,6 +140,12 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
139
140
|
: "PatchRelay is preparing or waiting to resume queue repair.",
|
|
140
141
|
});
|
|
141
142
|
break;
|
|
143
|
+
case "deploying":
|
|
144
|
+
observations.push({
|
|
145
|
+
tone: "info",
|
|
146
|
+
text: "PatchRelay merged the PR and is watching the deploy workflow on main.",
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
142
149
|
case "done":
|
|
143
150
|
observations.push({
|
|
144
151
|
tone: "success",
|
package/dist/db/issue-store.js
CHANGED
|
@@ -480,5 +480,8 @@ export function mapIssueRow(row) {
|
|
|
480
480
|
...(row.orchestration_settle_until !== null && row.orchestration_settle_until !== undefined
|
|
481
481
|
? { orchestrationSettleUntil: String(row.orchestration_settle_until) }
|
|
482
482
|
: {}),
|
|
483
|
+
...(row.deploy_started_at !== null && row.deploy_started_at !== undefined
|
|
484
|
+
? { deployStartedAt: String(row.deploy_started_at) }
|
|
485
|
+
: {}),
|
|
483
486
|
};
|
|
484
487
|
}
|
|
@@ -71,6 +71,7 @@ export const ISSUE_COLUMN_DEFS = {
|
|
|
71
71
|
zombieRecoveryAttempts: { column: "zombie_recovery_attempts", insertDefault: 0 },
|
|
72
72
|
lastZombieRecoveryAt: { column: "last_zombie_recovery_at" },
|
|
73
73
|
orchestrationSettleUntil: { column: "orchestration_settle_until" },
|
|
74
|
+
deployStartedAt: { column: "deploy_started_at" },
|
|
74
75
|
};
|
|
75
76
|
export const ISSUE_COLUMN_KEYS = Object.keys(ISSUE_COLUMN_DEFS);
|
|
76
77
|
/**
|
package/dist/db/migrations.js
CHANGED
|
@@ -359,6 +359,10 @@ export function runPatchRelayMigrations(connection) {
|
|
|
359
359
|
removeRetiredIssueColumnsIfPresent(connection);
|
|
360
360
|
addColumnIfMissing(connection, "issues", "issue_triage_hash", "TEXT");
|
|
361
361
|
addColumnIfMissing(connection, "issues", "issue_triage_result_json", "TEXT");
|
|
362
|
+
// PR3: post-merge deploy tracking. Timestamp the issue entered the
|
|
363
|
+
// `deploying` state, so the deploy watcher only considers deploy runs
|
|
364
|
+
// created at/after the merge (and can time out a never-arriving deploy).
|
|
365
|
+
addColumnIfMissing(connection, "issues", "deploy_started_at", "TEXT");
|
|
362
366
|
}
|
|
363
367
|
function addColumnIfMissing(connection, table, column, definition) {
|
|
364
368
|
const cols = connection.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { resolveClosedPrDisposition, resolveClosedPrFactoryState } from "./pr-state.js";
|
|
2
|
+
import { resolvePostMergeFactoryState } from "./post-merge-deploy.js";
|
|
2
3
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
3
4
|
import { syncGitHubLinearSession } from "./github-linear-session-sync.js";
|
|
4
5
|
import { wakeOrchestrationParentsForChildEvent } from "./orchestration-parent-wake.js";
|
|
5
6
|
export async function handleGitHubTerminalPrEvent(params) {
|
|
6
7
|
const { db, linearProvider, wakeDispatcher, logger, codex, issue, event, config } = params;
|
|
7
8
|
const eventType = event.triggerEvent === "pr_merged" ? "pr_merged" : "pr_closed";
|
|
9
|
+
// PR3: when the project configures a deploy workflow, a merge enters the
|
|
10
|
+
// `deploying` watch state instead of completing immediately. Linear
|
|
11
|
+
// completion is deferred until the deploy succeeds (idle reconciler).
|
|
12
|
+
const project = config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
13
|
+
const postMergeState = resolvePostMergeFactoryState(project);
|
|
8
14
|
db.issueSessions.appendIssueSessionEvent({
|
|
9
15
|
projectId: issue.projectId,
|
|
10
16
|
linearIssueId: issue.linearIssueId,
|
|
@@ -37,13 +43,14 @@ export async function handleGitHubTerminalPrEvent(params) {
|
|
|
37
43
|
});
|
|
38
44
|
}
|
|
39
45
|
const terminalFactoryState = event.triggerEvent === "pr_merged"
|
|
40
|
-
?
|
|
46
|
+
? postMergeState
|
|
41
47
|
: resolveClosedPrFactoryState(issue);
|
|
42
48
|
db.issues.upsertIssue({
|
|
43
49
|
projectId: issue.projectId,
|
|
44
50
|
linearIssueId: issue.linearIssueId,
|
|
45
51
|
activeRunId: null,
|
|
46
52
|
factoryState: terminalFactoryState,
|
|
53
|
+
...(terminalFactoryState === "deploying" ? { deployStartedAt: new Date().toISOString() } : {}),
|
|
47
54
|
});
|
|
48
55
|
};
|
|
49
56
|
const activeLease = db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
@@ -68,7 +75,12 @@ export async function handleGitHubTerminalPrEvent(params) {
|
|
|
68
75
|
eventType: "child_delivered",
|
|
69
76
|
wakeDispatcher,
|
|
70
77
|
});
|
|
71
|
-
|
|
78
|
+
// Only complete Linear now when there's no deploy to watch. While
|
|
79
|
+
// `deploying`, the issue stays in the Deploying state and the idle
|
|
80
|
+
// reconciler completes it once the deploy workflow succeeds.
|
|
81
|
+
if (postMergeState === "done") {
|
|
82
|
+
await completeLinearIssueAfterMerge(params, updatedIssue);
|
|
83
|
+
}
|
|
72
84
|
}
|
|
73
85
|
void syncGitHubLinearSession({
|
|
74
86
|
config,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
import { DEPLOY_WATCH_TIMEOUT_MS, evaluateDeploy, isDeployTrackingEnabled, } from "./post-merge-deploy.js";
|
|
1
3
|
import { buildBranchUpkeepContext, buildFailureContext, getGateCheckNames, hasCompletedReviewQuillVerdict, hasFailureProvenance, isDuplicateRepairAttempt, isFailingCheckStatus, isReviewDecisionApproved, isReviewDecisionReviewRequired, } from "./idle-reconciliation-helpers.js";
|
|
2
4
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
5
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
@@ -14,12 +16,16 @@ export class IdleIssueReconciler {
|
|
|
14
16
|
wakeDispatcher;
|
|
15
17
|
logger;
|
|
16
18
|
feed;
|
|
17
|
-
|
|
19
|
+
deployEvaluator;
|
|
20
|
+
constructor(db, config, wakeDispatcher, logger, feed,
|
|
21
|
+
// Injectable for tests; production uses the real `gh`-backed watcher.
|
|
22
|
+
deployEvaluator = evaluateDeploy) {
|
|
18
23
|
this.db = db;
|
|
19
24
|
this.config = config;
|
|
20
25
|
this.wakeDispatcher = wakeDispatcher;
|
|
21
26
|
this.logger = logger;
|
|
22
27
|
this.feed = feed;
|
|
28
|
+
this.deployEvaluator = deployEvaluator;
|
|
23
29
|
}
|
|
24
30
|
async reconcile() {
|
|
25
31
|
// Wrap the entire reconcile pass in a dispatcher tick. Every
|
|
@@ -34,7 +40,7 @@ export class IdleIssueReconciler {
|
|
|
34
40
|
async reconcileBody() {
|
|
35
41
|
for (const issue of this.db.issues.listIdleNonTerminalIssues()) {
|
|
36
42
|
if (issue.prState === "merged") {
|
|
37
|
-
this.
|
|
43
|
+
await this.handleMergedIssue(issue);
|
|
38
44
|
continue;
|
|
39
45
|
}
|
|
40
46
|
if (issue.lastGitHubFailureSource === "queue_eviction") {
|
|
@@ -119,8 +125,101 @@ export class IdleIssueReconciler {
|
|
|
119
125
|
return false;
|
|
120
126
|
if (issue.pendingRunType !== undefined)
|
|
121
127
|
return false;
|
|
128
|
+
// A merged PR cannot be un-merged: never re-probe it back toward the
|
|
129
|
+
// queue. This matters for deploy-failed issues (escalated while
|
|
130
|
+
// prState === "merged") — recovery-to-awaiting_queue would be wrong.
|
|
131
|
+
if (issue.prState === "merged")
|
|
132
|
+
return false;
|
|
122
133
|
return issue.factoryState === "escalated" || issue.factoryState === "failed";
|
|
123
134
|
}
|
|
135
|
+
// PR3: route a merged PR either into post-merge deploy tracking or
|
|
136
|
+
// straight to done. Called from both the idle pass and the GitHub
|
|
137
|
+
// reconcile path, so the deploying-vs-done decision lives in one place.
|
|
138
|
+
async handleMergedIssue(issue) {
|
|
139
|
+
if (issue.factoryState === "deploying") {
|
|
140
|
+
await this.watchDeploy(issue);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Already finalized (done/escalated/failed) — never re-open it.
|
|
144
|
+
if (TERMINAL_STATES.has(issue.factoryState))
|
|
145
|
+
return;
|
|
146
|
+
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
147
|
+
if (isDeployTrackingEnabled(project)) {
|
|
148
|
+
this.advanceIdleIssue(issue, "deploying", { clearFailureProvenance: true });
|
|
149
|
+
this.db.issues.upsertIssue({
|
|
150
|
+
projectId: issue.projectId,
|
|
151
|
+
linearIssueId: issue.linearIssueId,
|
|
152
|
+
deployStartedAt: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Poll the project's deploy workflow for a merged issue sitting in
|
|
160
|
+
// `deploying`: success → done, failure → escalate, still running → wait
|
|
161
|
+
// (with a timeout backstop so a never-arriving deploy can't strand it).
|
|
162
|
+
async watchDeploy(issue) {
|
|
163
|
+
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
164
|
+
const protocol = resolveMergeQueueProtocol(project);
|
|
165
|
+
const workflowName = protocol.deployWorkflowName;
|
|
166
|
+
const repoFullName = protocol.repoFullName;
|
|
167
|
+
if (!workflowName || !repoFullName) {
|
|
168
|
+
// Misconfigured / tracking disabled after entering — don't strand it.
|
|
169
|
+
this.finishDeploy(issue, "done");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const since = issue.deployStartedAt ?? issue.updatedAt;
|
|
173
|
+
const outcome = await this.deployEvaluator({
|
|
174
|
+
repoFullName,
|
|
175
|
+
workflowName,
|
|
176
|
+
baseBranch: protocol.baseBranch ?? "main",
|
|
177
|
+
sinceIso: since,
|
|
178
|
+
logger: this.logger,
|
|
179
|
+
});
|
|
180
|
+
if (outcome === "succeeded") {
|
|
181
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Deploy succeeded; completing issue");
|
|
182
|
+
this.finishDeploy(issue, "done");
|
|
183
|
+
this.feed?.publish({
|
|
184
|
+
level: "info",
|
|
185
|
+
kind: "stage",
|
|
186
|
+
issueKey: issue.issueKey,
|
|
187
|
+
projectId: issue.projectId,
|
|
188
|
+
stage: "done",
|
|
189
|
+
status: "deployed",
|
|
190
|
+
summary: `Deploy succeeded for PR #${issue.prNumber}`,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (outcome === "failed") {
|
|
195
|
+
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Deploy failed; escalating for operator attention");
|
|
196
|
+
this.finishDeploy(issue, "escalated");
|
|
197
|
+
this.feed?.publish({
|
|
198
|
+
level: "error",
|
|
199
|
+
kind: "workflow",
|
|
200
|
+
issueKey: issue.issueKey,
|
|
201
|
+
projectId: issue.projectId,
|
|
202
|
+
stage: "deploying",
|
|
203
|
+
status: "deploy_failed",
|
|
204
|
+
summary: `Deploy failed for PR #${issue.prNumber}; needs operator attention`,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Still pending — apply the timeout backstop.
|
|
209
|
+
const sinceMs = Date.parse(since);
|
|
210
|
+
if (Number.isFinite(sinceMs) && Date.now() - sinceMs > DEPLOY_WATCH_TIMEOUT_MS) {
|
|
211
|
+
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Deploy not observed within timeout; completing issue (change is already on main)");
|
|
212
|
+
this.finishDeploy(issue, "done");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finishDeploy(issue, state) {
|
|
216
|
+
this.advanceIdleIssue(issue, state, state === "done" ? { clearFailureProvenance: true } : undefined);
|
|
217
|
+
this.db.issues.upsertIssue({
|
|
218
|
+
projectId: issue.projectId,
|
|
219
|
+
linearIssueId: issue.linearIssueId,
|
|
220
|
+
deployStartedAt: null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
124
223
|
advanceIdleIssue(issue, newState, options) {
|
|
125
224
|
if (issue.factoryState === newState && !options?.pendingRunType && !options?.clearFailureProvenance) {
|
|
126
225
|
return;
|
|
@@ -367,7 +466,8 @@ export class IdleIssueReconciler {
|
|
|
367
466
|
});
|
|
368
467
|
if (pr.state === "MERGED") {
|
|
369
468
|
this.db.issues.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
370
|
-
this.
|
|
469
|
+
const merged = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? { ...issue, prState: "merged" };
|
|
470
|
+
await this.handleMergedIssue(merged);
|
|
371
471
|
return;
|
|
372
472
|
}
|
|
373
473
|
if (pr.state === "CLOSED") {
|
|
@@ -203,6 +203,8 @@ function statusHeadline(issue, activeRunType) {
|
|
|
203
203
|
return "Handed off downstream for merge";
|
|
204
204
|
case "repairing_queue":
|
|
205
205
|
return "Repairing merge handoff";
|
|
206
|
+
case "deploying":
|
|
207
|
+
return issue.prNumber !== undefined ? `Deploying merged PR #${issue.prNumber}` : "Deploying after merge";
|
|
206
208
|
case "awaiting_input":
|
|
207
209
|
return "Waiting for more input";
|
|
208
210
|
case "failed":
|
|
@@ -149,10 +149,9 @@ function resolveDesiredActiveWorkflowState(issue, trackedIssue, _options, liveIs
|
|
|
149
149
|
return resolvePreferredQueuedLinearState(liveIssue);
|
|
150
150
|
}
|
|
151
151
|
// 4. Post-merge: the change is on main, deploy running → Deploying.
|
|
152
|
-
// Durable
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
if (normalize(issue.prState) === "merged") {
|
|
152
|
+
// Durable signals: factoryState === "deploying" (post-merge deploy
|
|
153
|
+
// watch in progress) or the PR is merged but not yet done.
|
|
154
|
+
if (issue.factoryState === "deploying" || normalize(issue.prState) === "merged") {
|
|
156
155
|
return resolvePreferredDeployingLinearState(liveIssue);
|
|
157
156
|
}
|
|
158
157
|
// 5. Patchrelay is actively addressing review/CI/queue feedback →
|
|
@@ -19,5 +19,6 @@ export function resolveMergeQueueProtocol(project) {
|
|
|
19
19
|
specBranchPattern: project?.github?.specBranchPattern ?? DEFAULT_SPEC_BRANCH_PATTERN,
|
|
20
20
|
noCacheLabel: project?.github?.noCacheLabel ?? DEFAULT_NO_CACHE_LABEL,
|
|
21
21
|
queuedForDeployLabel: project?.github?.queuedForDeployLabel ?? DEFAULT_QUEUED_FOR_DEPLOY_LABEL,
|
|
22
|
+
deployWorkflowName: project?.github?.deployWorkflowName,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
|
+
import { execCommand } from "./utils.js";
|
|
3
|
+
// How long an issue may sit in `deploying` before we give up watching and
|
|
4
|
+
// advance to `done` anyway. The change is already on `main`; if no deploy
|
|
5
|
+
// run ever shows up (no workflow triggered, or it was superseded and GC'd),
|
|
6
|
+
// we must not strand the issue. 20 minutes comfortably covers a queued +
|
|
7
|
+
// running deploy without leaving issues stuck for hours.
|
|
8
|
+
export const DEPLOY_WATCH_TIMEOUT_MS = 20 * 60_000;
|
|
9
|
+
// Small grace window: a deploy run triggered by the merge push can be
|
|
10
|
+
// created a few seconds before we stamp `deployStartedAt`. Don't exclude it.
|
|
11
|
+
const SINCE_GRACE_MS = 2 * 60_000;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a merge should enter the `deploying` watch state. Opt-in per
|
|
14
|
+
* project via `github.deployWorkflowName`; absent → advance straight to
|
|
15
|
+
* `done` (today's behavior, no risk of stranding issues).
|
|
16
|
+
*/
|
|
17
|
+
export function isDeployTrackingEnabled(project) {
|
|
18
|
+
return Boolean(resolveMergeQueueProtocol(project).deployWorkflowName);
|
|
19
|
+
}
|
|
20
|
+
export function resolvePostMergeFactoryState(project) {
|
|
21
|
+
return isDeployTrackingEnabled(project) ? "deploying" : "done";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decide the deploy outcome from the recent runs of the deploy workflow on
|
|
25
|
+
* the base branch. Pure and total so it can be unit-tested without GitHub.
|
|
26
|
+
*
|
|
27
|
+
* Only runs created at/after `sinceIso` (minus a small grace) count — any
|
|
28
|
+
* deploy on `main` after the merge includes the merged change, since `main`
|
|
29
|
+
* only moves forward. The most recent decisive run wins; cancelled/skipped
|
|
30
|
+
* runs are ignored (a later run supersedes them).
|
|
31
|
+
*/
|
|
32
|
+
export function interpretDeployRuns(runs, sinceIso) {
|
|
33
|
+
const sinceMs = Date.parse(sinceIso);
|
|
34
|
+
const cutoff = Number.isFinite(sinceMs) ? sinceMs - SINCE_GRACE_MS : -Infinity;
|
|
35
|
+
const relevant = runs
|
|
36
|
+
.filter((r) => {
|
|
37
|
+
const t = Date.parse(r.createdAt);
|
|
38
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
41
|
+
for (const run of relevant) {
|
|
42
|
+
const conclusion = (run.conclusion ?? "").toLowerCase();
|
|
43
|
+
const status = run.status.toLowerCase();
|
|
44
|
+
if (status !== "completed") {
|
|
45
|
+
// queued / in_progress / waiting / requested → still deploying.
|
|
46
|
+
return "pending";
|
|
47
|
+
}
|
|
48
|
+
if (conclusion === "success")
|
|
49
|
+
return "succeeded";
|
|
50
|
+
if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "startup_failure") {
|
|
51
|
+
return "failed";
|
|
52
|
+
}
|
|
53
|
+
// cancelled / skipped / neutral / stale / action_required — not decisive;
|
|
54
|
+
// look at the next-most-recent run.
|
|
55
|
+
}
|
|
56
|
+
return "pending";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Query the deploy workflow's recent runs on the base branch and interpret
|
|
60
|
+
* them. Returns "pending" on any query error so the watcher simply retries
|
|
61
|
+
* next tick (and the timeout backstops a permanently-absent deploy).
|
|
62
|
+
*/
|
|
63
|
+
export async function evaluateDeploy(params) {
|
|
64
|
+
const { repoFullName, workflowName, baseBranch, sinceIso, logger } = params;
|
|
65
|
+
try {
|
|
66
|
+
const { stdout } = await execCommand("gh", [
|
|
67
|
+
"run", "list",
|
|
68
|
+
"--repo", repoFullName,
|
|
69
|
+
"--workflow", workflowName,
|
|
70
|
+
"--branch", baseBranch,
|
|
71
|
+
"--json", "status,conclusion,createdAt",
|
|
72
|
+
"-L", "15",
|
|
73
|
+
], { timeoutMs: 15_000 });
|
|
74
|
+
const runs = JSON.parse(stdout);
|
|
75
|
+
if (!Array.isArray(runs))
|
|
76
|
+
return "pending";
|
|
77
|
+
return interpretDeployRuns(runs, sinceIso);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger?.debug({ repoFullName, workflowName, error: error instanceof Error ? error.message : String(error) }, "Deploy watch query failed; will retry");
|
|
81
|
+
return "pending";
|
|
82
|
+
}
|
|
83
|
+
}
|