patchrelay 0.71.2 → 0.72.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 +3 -3
- package/dist/linear-workflow-state-sync.js +108 -45
- package/dist/linear-workflow.js +24 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolvePreferredQueuedLinearState, resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState,
|
|
1
|
+
import { resolvePreferredQueuedLinearState, resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredMergeQueueLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
2
2
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
3
|
import { isCompletedLinearState } from "./pr-state.js";
|
|
4
4
|
import { hasTrustedNoPrCompletion } from "./trusted-no-pr-completion.js";
|
|
@@ -67,24 +67,21 @@ async function syncQueuedForDeployLabel(params) {
|
|
|
67
67
|
await linear.updateIssueLabels({ issueId: issue.linearIssueId, removeNames: [labelName] });
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
// True only when (a) the issue is
|
|
71
|
-
// Linear workflow has no In
|
|
72
|
-
// preferred-
|
|
73
|
-
//
|
|
74
|
-
// state, `setIssueState` flows the issue there and
|
|
75
|
-
// unnecessary.
|
|
70
|
+
// True only when (a) the issue is in the merge queue (`awaiting_queue`)
|
|
71
|
+
// AND (b) the project's Linear workflow has no dedicated In Merge Queue
|
|
72
|
+
// state — detected by the preferred merge-queue state collapsing to the
|
|
73
|
+
// same name as the reviewing state. When the project has a real In Merge
|
|
74
|
+
// Queue (or Deploying) state, `setIssueState` flows the issue there and
|
|
75
|
+
// the label is unnecessary.
|
|
76
76
|
function isQueuedForDeployFallback(issue, liveIssue) {
|
|
77
77
|
if (issue.factoryState !== "awaiting_queue")
|
|
78
78
|
return false;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (!deploying || !review)
|
|
79
|
+
const mergeQueue = resolvePreferredMergeQueueLinearState(liveIssue);
|
|
80
|
+
const reviewing = resolvePreferredReviewingLinearState(liveIssue);
|
|
81
|
+
if (!mergeQueue || !reviewing)
|
|
83
82
|
return false;
|
|
84
|
-
// No
|
|
85
|
-
|
|
86
|
-
return deploying.trim().toLowerCase() === review.trim().toLowerCase()
|
|
87
|
-
&& (deployUnstarted ?? "").trim().toLowerCase() === review.trim().toLowerCase();
|
|
83
|
+
// No dedicated merge-queue state → it collapses to the reviewing state.
|
|
84
|
+
return mergeQueue.trim().toLowerCase() === reviewing.trim().toLowerCase();
|
|
88
85
|
}
|
|
89
86
|
async function syncCompletedLinearState(params) {
|
|
90
87
|
const { db, issue, linear, liveIssue } = params;
|
|
@@ -121,49 +118,115 @@ function shouldAutoAdvanceLinearState(issue) {
|
|
|
121
118
|
const normalizedName = issue.currentLinearState?.trim().toLowerCase();
|
|
122
119
|
return normalizedName !== "done" && normalizedName !== "completed" && normalizedName !== "complete";
|
|
123
120
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
// ─── Unified PR-lifecycle → Linear-state mapping ─────────────────────
|
|
122
|
+
//
|
|
123
|
+
// Five phases, in lifecycle order:
|
|
124
|
+
// Implementing → Reviewing → In Merge Queue → Deploying → Done
|
|
125
|
+
//
|
|
126
|
+
// Every phase is decided from DURABLE signals (factoryState, prState,
|
|
127
|
+
// prReviewState) — never the ephemeral activeRunId / sessionState / run
|
|
128
|
+
// type. That is what kills the Implementing↔Reviewing flap: the state
|
|
129
|
+
// only moves on a real lifecycle handoff (a review verdict, an approval,
|
|
130
|
+
// a merge), not on whichever transient webhook happens to recompute it
|
|
131
|
+
// while a run briefly holds a lease.
|
|
132
|
+
//
|
|
133
|
+
// Branches are ordered "furthest along the lifecycle wins" so a stale
|
|
134
|
+
// earlier signal can never pull a more-advanced issue backwards.
|
|
135
|
+
function resolveDesiredActiveWorkflowState(issue, trackedIssue, _options, liveIssue) {
|
|
136
|
+
// 1. Operator must act — overrides everything.
|
|
137
|
+
if (needsHumanAttention(issue, trackedIssue)) {
|
|
127
138
|
return resolvePreferredHumanNeededLinearState(liveIssue);
|
|
128
139
|
}
|
|
140
|
+
// 2. Completed → Done. Covers today's merge→done path (the factory has
|
|
141
|
+
// no post-merge state yet), so a done issue never reads as Deploying.
|
|
142
|
+
if (issue.factoryState === "done") {
|
|
143
|
+
return resolvePreferredCompletedLinearState(liveIssue);
|
|
144
|
+
}
|
|
145
|
+
// 3. Paused with no PR and nothing for us to do → backlog.
|
|
129
146
|
const blocked = (trackedIssue?.blockedByCount ?? 0) > 0;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
147
|
+
const noPr = issue.prNumber === undefined && !issue.prUrl;
|
|
148
|
+
if (noPr && (issue.delegatedToPatchRelay === false || blocked)) {
|
|
132
149
|
return resolvePreferredQueuedLinearState(liveIssue);
|
|
133
150
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
// 4. Post-merge: the change is on main, deploy running → Deploying.
|
|
152
|
+
// Durable signal: the PR is merged. (Until PR3 makes merge a
|
|
153
|
+
// non-terminal phase this only fires in the merged-not-yet-done
|
|
154
|
+
// window; branch 2 already caught factoryState==="done".)
|
|
155
|
+
if (normalize(issue.prState) === "merged") {
|
|
156
|
+
return resolvePreferredDeployingLinearState(liveIssue);
|
|
157
|
+
}
|
|
158
|
+
// 5. Patchrelay is actively addressing review/CI/queue feedback →
|
|
159
|
+
// Implementing. These factory states persist for the run's whole
|
|
160
|
+
// duration, so this is stable, not flappy — and it is exactly the
|
|
161
|
+
// "show when patchrelay handles feedback" behavior we want.
|
|
162
|
+
if (isAddressingFeedback(issue)) {
|
|
143
163
|
return resolvePreferredImplementingLinearState(liveIssue);
|
|
144
164
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return resolvePreferredDeployingLinearState(liveIssue);
|
|
165
|
+
// 6. Approved / admitted to the merge queue → In Merge Queue.
|
|
166
|
+
if (isInMergeQueue(issue)) {
|
|
167
|
+
return resolvePreferredMergeQueueLinearState(liveIssue);
|
|
149
168
|
}
|
|
150
|
-
|
|
169
|
+
// 7. Pre-review-feedback implementation work (incl. a draft PR) →
|
|
170
|
+
// Implementing.
|
|
171
|
+
if (isImplementing(issue, trackedIssue)) {
|
|
172
|
+
return resolvePreferredImplementingLinearState(liveIssue);
|
|
173
|
+
}
|
|
174
|
+
// 8. PR exists and is under review → Reviewing.
|
|
175
|
+
if (isReviewBound(issue)) {
|
|
151
176
|
return resolvePreferredReviewingLinearState(liveIssue);
|
|
152
177
|
}
|
|
153
|
-
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function normalize(value) {
|
|
181
|
+
const trimmed = value?.trim().toLowerCase();
|
|
182
|
+
return trimmed ? trimmed : undefined;
|
|
183
|
+
}
|
|
184
|
+
function needsHumanAttention(issue, trackedIssue) {
|
|
185
|
+
return issue.factoryState === "awaiting_input"
|
|
186
|
+
|| issue.factoryState === "failed"
|
|
187
|
+
|| issue.factoryState === "escalated"
|
|
188
|
+
|| trackedIssue?.sessionState === "waiting_input"
|
|
189
|
+
|| trackedIssue?.sessionState === "failed";
|
|
190
|
+
}
|
|
191
|
+
// Active code work to address feedback. Durable factory states +
|
|
192
|
+
// changes-requested review verdict — no run-id involvement. Gated on
|
|
193
|
+
// delegation: an undelegated PR (operator paused us) is not being worked
|
|
194
|
+
// by patchrelay, so it must not read as Implementing.
|
|
195
|
+
function isAddressingFeedback(issue) {
|
|
196
|
+
if (issue.delegatedToPatchRelay === false)
|
|
197
|
+
return false;
|
|
198
|
+
return issue.factoryState === "changes_requested"
|
|
199
|
+
|| issue.factoryState === "repairing_ci"
|
|
200
|
+
|| issue.factoryState === "repairing_queue"
|
|
201
|
+
|| normalize(issue.prReviewState) === "changes_requested";
|
|
202
|
+
}
|
|
203
|
+
// Approved and heading to / sitting in the merge queue. Not yet merged
|
|
204
|
+
// (branch 4 catches merged first).
|
|
205
|
+
function isInMergeQueue(issue) {
|
|
206
|
+
return issue.factoryState === "awaiting_queue"
|
|
207
|
+
|| normalize(issue.prReviewState) === "approved";
|
|
208
|
+
}
|
|
209
|
+
// Initial implementation, before review starts. A draft PR still counts
|
|
210
|
+
// as implementing. Gated on delegation so we never claim Implementing
|
|
211
|
+
// for work that isn't ours.
|
|
212
|
+
function isImplementing(issue, trackedIssue) {
|
|
213
|
+
if (issue.delegatedToPatchRelay === false)
|
|
214
|
+
return false;
|
|
215
|
+
if (issue.factoryState === "implementing")
|
|
216
|
+
return true;
|
|
217
|
+
if (issue.factoryState === "delegated") {
|
|
218
|
+
const blocked = (trackedIssue?.blockedByCount ?? 0) > 0;
|
|
219
|
+
return !blocked && trackedIssue?.readyForExecution !== false;
|
|
220
|
+
}
|
|
221
|
+
return issue.prIsDraft === true;
|
|
222
|
+
}
|
|
223
|
+
function isReviewBound(issue) {
|
|
224
|
+
return issue.prNumber !== undefined
|
|
154
225
|
|| Boolean(issue.prUrl)
|
|
155
226
|
|| issue.factoryState === "pr_open"
|
|
156
227
|
|| issue.prReviewState !== undefined
|
|
157
|
-
|| issue.prCheckStatus !== undefined
|
|
158
|
-
|
|
159
|
-
return resolvePreferredReviewLinearState(liveIssue);
|
|
160
|
-
}
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
function isApprovedAndGreen(prReviewState, prCheckStatus) {
|
|
164
|
-
const normalizedReview = prReviewState?.trim().toLowerCase();
|
|
165
|
-
const normalizedChecks = prCheckStatus?.trim().toLowerCase();
|
|
166
|
-
return normalizedReview === "approved" && (normalizedChecks === "success" || normalizedChecks === "passed");
|
|
228
|
+
|| issue.prCheckStatus !== undefined
|
|
229
|
+
|| hasPendingReviewQuillVerdict(issue.lastGitHubCiSnapshotJson);
|
|
167
230
|
}
|
|
168
231
|
function hasPendingReviewQuillVerdict(snapshotJson) {
|
|
169
232
|
if (!snapshotJson)
|
package/dist/linear-workflow.js
CHANGED
|
@@ -59,16 +59,37 @@ export function resolvePreferredReviewingLinearState(issue) {
|
|
|
59
59
|
fallback: resolvePreferredReviewLinearState(issue),
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
// The pre-merge "approved, awaiting/undergoing landing" phase. Covers a
|
|
63
|
+
// PR that is queued, being tested in the speculative branch, or actively
|
|
64
|
+
// merging — i.e. everything the merge queue owns up to (but not past)
|
|
65
|
+
// the merge. NOT post-merge: that is `resolvePreferredDeployingLinearState`.
|
|
66
|
+
// Without a dedicated queue state, collapses to the reviewing state (and
|
|
67
|
+
// the `queued-for-deploy` label disambiguates — see state-sync).
|
|
68
|
+
export function resolvePreferredMergeQueueLinearState(issue) {
|
|
69
|
+
return resolvePreferredLinearState(issue, {
|
|
70
|
+
names: ["in merge queue", "merge queue", "in queue", "queued", "queue", "merging", "landing", "ready to merge"],
|
|
71
|
+
types: ["started"],
|
|
72
|
+
fallback: resolvePreferredLinearState(issue, {
|
|
73
|
+
names: ["in merge queue", "merge queue", "queued", "ready to merge", "ready to deploy", "ready for deploy", "to deploy", "merge"],
|
|
74
|
+
types: ["unstarted"],
|
|
75
|
+
fallback: resolvePreferredReviewingLinearState(issue),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Unstarted deploy column, used as a fallback by the started variant.
|
|
62
80
|
export function resolvePreferredDeployLinearState(issue) {
|
|
63
81
|
return resolvePreferredLinearState(issue, {
|
|
64
|
-
names: ["deploy", "
|
|
82
|
+
names: ["deploy", "to deploy", "ready to ship"],
|
|
65
83
|
types: ["unstarted"],
|
|
66
|
-
fallback:
|
|
84
|
+
fallback: resolvePreferredMergeQueueLinearState(issue),
|
|
67
85
|
});
|
|
68
86
|
}
|
|
87
|
+
// Strictly POST-merge: the change is on main and the deploy workflow is
|
|
88
|
+
// running. "merging" lives in the merge-queue phase, not here. Without a
|
|
89
|
+
// dedicated deploy state, collapses back to the merge-queue state.
|
|
69
90
|
export function resolvePreferredDeployingLinearState(issue) {
|
|
70
91
|
return resolvePreferredLinearState(issue, {
|
|
71
|
-
names: ["deploying", "
|
|
92
|
+
names: ["deploying", "deployment", "in deploy", "shipping", "releasing", "rollout"],
|
|
72
93
|
types: ["started"],
|
|
73
94
|
fallback: resolvePreferredDeployLinearState(issue),
|
|
74
95
|
});
|