patchrelay 0.70.0 → 0.71.1

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.70.0",
4
- "commit": "b70636ecd6d4",
5
- "builtAt": "2026-05-23T19:04:31.282Z"
3
+ "version": "0.71.1",
4
+ "commit": "a52a4e7f91af",
5
+ "builtAt": "2026-05-23T22:11:42.868Z"
6
6
  }
@@ -1,6 +1,5 @@
1
- import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
2
1
  import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
3
- import { buildMainRepairBranchName, buildMainRepairDescription, buildMainRepairPromptContext, buildMainRepairTitle, isMainRepairIssue, } from "./main-repair.js";
2
+ import { buildMainRepairBranchName, isMainRepairIssue, } from "./main-repair.js";
4
3
  import { execCommand } from "./utils.js";
5
4
  const MAIN_BRANCH_HEALTH_GRACE_MS = 120_000;
6
5
  function isUnhealthyMainConclusion(conclusion) {
@@ -14,14 +13,14 @@ export class MainBranchHealthMonitor {
14
13
  db;
15
14
  config;
16
15
  linearProvider;
17
- wakeDispatcher;
18
16
  logger;
19
17
  feed;
20
- constructor(db, config, linearProvider, wakeDispatcher, logger, feed) {
18
+ /** Per-project throttle for the information-only "main is red" log. */
19
+ lastUnhealthyReportAt = new Map();
20
+ constructor(db, config, linearProvider, logger, feed) {
21
21
  this.db = db;
22
22
  this.config = config;
23
23
  this.linearProvider = linearProvider;
24
- this.wakeDispatcher = wakeDispatcher;
25
24
  this.logger = logger;
26
25
  this.feed = feed;
27
26
  }
@@ -46,64 +45,27 @@ export class MainBranchHealthMonitor {
46
45
  }
47
46
  return;
48
47
  }
49
- const protocol = resolveMergeQueueProtocol(project);
50
- if (existing) {
51
- const age = Date.now() - Date.parse(existing.updatedAt);
52
- if (age < MAIN_BRANCH_HEALTH_GRACE_MS) {
53
- return;
54
- }
55
- }
56
- if (existing) {
57
- this.queueExistingMainRepair(existing, summary, protocol.priorityLabel);
58
- return;
59
- }
60
- const client = await this.linearProvider.forProject(projectId);
61
- if (!client?.createIssue) {
62
- this.logger.warn({ projectId, repoFullName: project.github.repoFullName }, "Cannot create main repair issue because Linear issue creation is unavailable");
48
+ // main CI is red. The merge queue (merge-steward) gates only on its own
49
+ // speculative-SHA checks and ignores main entirely, so a red main no longer
50
+ // warrants an automated repair job — main CI is information-only. Report it
51
+ // (throttled) and post nothing. Any pre-existing repair issue is left to close
52
+ // via resolveRecoveredMainRepair once main recovers.
53
+ this.reportUnhealthyMain(projectId, project.github.repoFullName, baseBranch, summary);
54
+ }
55
+ reportUnhealthyMain(projectId, repoFullName, baseBranch, summary) {
56
+ const now = Date.now();
57
+ const lastReportedAt = this.lastUnhealthyReportAt.get(projectId);
58
+ if (lastReportedAt !== undefined && now - lastReportedAt < MAIN_BRANCH_HEALTH_GRACE_MS) {
63
59
  return;
64
60
  }
65
- const created = await client.createIssue({
66
- teamId: project.linearTeamIds[0],
67
- title: buildMainRepairTitle(project),
68
- description: buildMainRepairDescription(project, summary, protocol.priorityLabel),
69
- });
70
- const issue = this.db.upsertIssue({
61
+ this.lastUnhealthyReportAt.set(projectId, now);
62
+ this.logger.warn({
71
63
  projectId,
72
- linearIssueId: created.id,
73
- delegatedToPatchRelay: true,
74
- ...(created.identifier ? { issueKey: created.identifier } : {}),
75
- ...(created.title ? { title: created.title } : {}),
76
- ...(created.description ? { description: created.description } : {}),
77
- ...(created.url ? { url: created.url } : {}),
78
- ...(created.priority != null ? { priority: created.priority } : {}),
79
- ...(created.estimate != null ? { estimate: created.estimate } : {}),
80
- ...(created.stateName ? { currentLinearState: created.stateName } : {}),
81
- ...(created.stateType ? { currentLinearStateType: created.stateType } : {}),
82
- branchName,
83
- factoryState: "delegated",
84
- });
85
- this.wakeDispatcher.recordEventAndDispatch(projectId, issue.linearIssueId, {
86
- eventType: "delegated",
87
- eventJson: JSON.stringify({
88
- runType: "main_repair",
89
- baseSha: summary.baseSha,
90
- failingChecks: summary.failingChecks,
91
- pendingChecks: summary.pendingChecks,
92
- priorityLabel: protocol.priorityLabel,
93
- promptContext: buildMainRepairPromptContext(project, summary, protocol.priorityLabel),
94
- }),
95
- dedupeKey: `main_repair:${projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
96
- });
97
- this.feed?.publish({
98
- level: "warn",
99
- kind: "github",
100
- issueKey: issue.issueKey,
101
- projectId,
102
- stage: "delegated",
103
- status: "main_repair_queued",
104
- summary: `Queued main_repair for ${project.github.repoFullName}@${baseBranch}`,
105
- detail: summary.failingChecks.map((check) => check.name).join(", "),
106
- });
64
+ repoFullName,
65
+ baseBranch,
66
+ baseSha: summary.baseSha,
67
+ failingChecks: summary.failingChecks.map((check) => check.name),
68
+ }, "main branch CI is red — information only; no repair job posted (merge queue gates on its own spec CI)");
107
69
  }
108
70
  findExistingMainRepair(projectId, branchName) {
109
71
  const candidates = this.db.listIssues()
@@ -132,35 +94,6 @@ export class MainBranchHealthMonitor {
132
94
  return 3;
133
95
  return 4;
134
96
  }
135
- queueExistingMainRepair(issue, summary, priorityLabel) {
136
- if (issue.activeRunId !== undefined)
137
- return;
138
- if (this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId))
139
- return;
140
- if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open")
141
- return;
142
- this.db.upsertIssue({
143
- projectId: issue.projectId,
144
- linearIssueId: issue.linearIssueId,
145
- delegatedToPatchRelay: true,
146
- factoryState: "delegated",
147
- pendingRunType: null,
148
- pendingRunContextJson: null,
149
- activeRunId: null,
150
- });
151
- this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
152
- eventType: "delegated",
153
- eventJson: JSON.stringify({
154
- runType: "main_repair",
155
- baseSha: summary.baseSha,
156
- failingChecks: summary.failingChecks,
157
- pendingChecks: summary.pendingChecks,
158
- priorityLabel,
159
- promptContext: buildMainRepairPromptContext(this.config.projects.find((project) => project.id === issue.projectId) ?? { id: issue.projectId }, summary, priorityLabel),
160
- }),
161
- dedupeKey: `main_repair:${issue.projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
162
- });
163
- }
164
97
  async resolveRecoveredMainRepair(issue) {
165
98
  if (issue.activeRunId !== undefined)
166
99
  return;
@@ -137,7 +137,7 @@ export class RunOrchestrator {
137
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
- this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, this.wakeDispatcher, logger, feed);
140
+ this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, logger, feed);
141
141
  this.mergedLinearCompletionReconciler = new MergedLinearCompletionReconciler(db, linearProvider, logger);
142
142
  this.queueHealthMonitor = new QueueHealthMonitor(db, config, {
143
143
  advanceIdleIssue: (issue, newState, options) => this.idleReconciler.advanceIdleIssue(issue, newState, options),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.70.0",
3
+ "version": "0.71.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,8 +12,11 @@ LINEAR_OAUTH_CLIENT_ID=replace-with-linear-oauth-client-id
12
12
  LINEAR_OAUTH_CLIENT_SECRET=replace-with-linear-oauth-client-secret
13
13
 
14
14
  # Optional: GitHub App for bot identity.
15
- # When configured, PatchRelay generates short-lived installation tokens
16
- # and writes a gh wrapper so Codex operates as app-name[bot].
15
+ # When configured, PatchRelay mints short-lived installation tokens and keeps a
16
+ # private gh config dir fresh (GH_CONFIG_DIR), so both gh and git authenticate as
17
+ # app-name[bot] with an always-fresh token. git reads credentials from gh via the
18
+ # credential helper; nothing is written into repo or global git config, so these
19
+ # credentials never leak into interactive shell sessions.
17
20
  # Create a GitHub App at Settings > Developer settings > GitHub Apps.
18
21
  #
19
22
  # The private key resolves through the provider-agnostic fallback: