patchrelay 0.35.4 → 0.35.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.35.4",
4
- "commit": "327273902c17",
5
- "builtAt": "2026-04-03T08:18:26.141Z"
3
+ "version": "0.35.5",
4
+ "commit": "9abdef5af6a1",
5
+ "builtAt": "2026-04-03T11:16:59.270Z"
6
6
  }
@@ -218,6 +218,8 @@ export function runPatchRelayMigrations(connection) {
218
218
  addColumnIfMissing(connection, "issues", "last_queue_incident_json", "TEXT");
219
219
  addColumnIfMissing(connection, "issues", "last_attempted_failure_head_sha", "TEXT");
220
220
  addColumnIfMissing(connection, "issues", "last_attempted_failure_signature", "TEXT");
221
+ // Track whether the merge queue label was successfully applied.
222
+ addColumnIfMissing(connection, "issues", "queue_label_applied", "INTEGER NOT NULL DEFAULT 0");
221
223
  }
222
224
  function addColumnIfMissing(connection, table, column, definition) {
223
225
  const cols = connection.prepare(`PRAGMA table_info(${table})`).all();
package/dist/db.js CHANGED
@@ -245,6 +245,10 @@ export class PatchRelayDatabase {
245
245
  sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
246
246
  values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
247
247
  }
248
+ if (params.queueLabelApplied !== undefined) {
249
+ sets.push("queue_label_applied = @queueLabelApplied");
250
+ values.queueLabelApplied = params.queueLabelApplied ? 1 : 0;
251
+ }
248
252
  this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
249
253
  }
250
254
  else {
@@ -743,6 +747,7 @@ function mapIssueRow(row) {
743
747
  reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
744
748
  zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
745
749
  ...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
750
+ queueLabelApplied: Boolean(row.queue_label_applied),
746
751
  };
747
752
  }
748
753
  function mapRunRow(row) {
@@ -12,7 +12,7 @@ export function resolveMergeQueueProtocol(project) {
12
12
  export async function requestMergeQueueAdmission(params) {
13
13
  const { issue, protocol, logger, feed } = params;
14
14
  if (!protocol.repoFullName || !issue.prNumber)
15
- return;
15
+ return false;
16
16
  feed?.publish({
17
17
  level: "info",
18
18
  kind: "github",
@@ -42,6 +42,7 @@ export async function requestMergeQueueAdmission(params) {
42
42
  status: "queue_label_applied",
43
43
  summary: `Queue label "${protocol.admissionLabel}" applied to PR #${issue.prNumber}`,
44
44
  });
45
+ return true;
45
46
  }
46
47
  catch (error) {
47
48
  logger.warn({ issueKey: issue.issueKey, err: error }, "Failed to add merge queue label");
@@ -55,5 +56,6 @@ export async function requestMergeQueueAdmission(params) {
55
56
  summary: `Queue hand-off failed while adding label "${protocol.admissionLabel}" to PR #${issue.prNumber}`,
56
57
  detail: error instanceof Error ? error.message : String(error),
57
58
  });
59
+ return false;
58
60
  }
59
61
  }
@@ -703,6 +703,10 @@ export class RunOrchestrator {
703
703
  if (issue.factoryState !== "awaiting_queue" || issue.branchOwner !== "merge_steward") {
704
704
  this.advanceIdleIssue(issue, "awaiting_queue", { clearFailureProvenance: true });
705
705
  }
706
+ else if (!issue.queueLabelApplied) {
707
+ // Retry failed label application
708
+ await this.requestMergeQueueAdmission(issue, issue.projectId);
709
+ }
706
710
  continue;
707
711
  }
708
712
  // Checks failed + idle — route based on durable GitHub failure provenance.
@@ -734,15 +738,29 @@ export class RunOrchestrator {
734
738
  continue;
735
739
  }
736
740
  if (issue.factoryState === "awaiting_queue") {
737
- this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation skipped failed awaiting_queue issue with unknown failure provenance");
738
- this.feed?.publish({
739
- level: "warn",
740
- kind: "github",
741
- issueKey: issue.issueKey,
742
- projectId: issue.projectId,
743
- stage: issue.factoryState,
744
- status: "failure_source_unknown",
745
- summary: "Reconciliation saw failed checks but could not determine whether the failure came from CI or the merge queue",
741
+ // Infer provenance: check if steward eviction check run exists on the PR
742
+ const inferProject = this.config.projects.find((p) => p.id === issue.projectId);
743
+ const inferProtocol = resolveMergeQueueProtocol(inferProject);
744
+ let inferred = "branch_ci";
745
+ if (inferProject?.github?.repoFullName && issue.prNumber && issue.lastGitHubFailureHeadSha) {
746
+ try {
747
+ const { stdout } = await execCommand("gh", [
748
+ "api",
749
+ `repos/${inferProject.github.repoFullName}/commits/${issue.lastGitHubFailureHeadSha}/check-runs`,
750
+ "--jq", `.check_runs[] | select(.name == "${inferProtocol.evictionCheckName}" and .conclusion == "failure") | .name`,
751
+ ], { timeoutMs: 10_000 });
752
+ if (stdout.trim().length > 0)
753
+ inferred = "queue_eviction";
754
+ }
755
+ catch { /* best effort */ }
756
+ }
757
+ const inferRunType = inferred === "queue_eviction" ? "queue_repair" : "ci_repair";
758
+ const inferState = inferred === "queue_eviction" ? "repairing_queue" : "repairing_ci";
759
+ this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, inferred }, "Inferred failure provenance for awaiting_queue issue");
760
+ const pendingRunContext = buildFailureContext(issue);
761
+ this.advanceIdleIssue(issue, inferState, {
762
+ pendingRunType: inferRunType,
763
+ ...(pendingRunContext ? { pendingRunContext } : {}),
746
764
  });
747
765
  continue;
748
766
  }
@@ -1117,15 +1135,18 @@ export class RunOrchestrator {
1117
1135
  void this.syncLinearSession(escalatedIssue);
1118
1136
  }
1119
1137
  /** Add the merge queue admission label for external-queue projects (best-effort). */
1120
- requestMergeQueueAdmission(issue, projectId) {
1138
+ async requestMergeQueueAdmission(issue, projectId) {
1121
1139
  const project = this.config.projects.find((p) => p.id === projectId);
1122
1140
  const protocol = resolveMergeQueueProtocol(project);
1123
- void requestMergeQueueAdmission({
1141
+ const applied = await requestMergeQueueAdmission({
1124
1142
  issue,
1125
1143
  protocol,
1126
1144
  logger: this.logger,
1127
1145
  feed: this.feed,
1128
1146
  });
1147
+ if (applied) {
1148
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, queueLabelApplied: true });
1149
+ }
1129
1150
  }
1130
1151
  failRunAndClear(run, message, nextState = "failed") {
1131
1152
  this.db.transaction(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.35.4",
3
+ "version": "0.35.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {