patchrelay 0.12.2 → 0.12.4
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/factory-state.js +93 -47
- package/dist/github-webhook-handler.js +5 -27
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/factory-state.js
CHANGED
|
@@ -10,52 +10,98 @@ export const TERMINAL_STATES = new Set([
|
|
|
10
10
|
"done",
|
|
11
11
|
"escalated",
|
|
12
12
|
]);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
13
|
+
// ─── Semantic guards ─────────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// Guards express INTENT rather than enumerating states. Adding a new
|
|
16
|
+
// state automatically participates in transitions whose guard it
|
|
17
|
+
// satisfies — no per-event maintenance required.
|
|
18
|
+
const isOpen = (s) => !TERMINAL_STATES.has(s);
|
|
19
|
+
const TRANSITION_RULES = [
|
|
20
|
+
// ── Terminal events ────────────────────────────────────────────
|
|
21
|
+
// pr_merged is unconditional — PR is merged, we're done.
|
|
22
|
+
{ event: "pr_merged",
|
|
23
|
+
to: "done" },
|
|
24
|
+
// pr_closed during an active run is suppressed — Codex may reopen.
|
|
25
|
+
// Without a guard match, the event produces no transition (undefined).
|
|
26
|
+
{ event: "pr_closed",
|
|
27
|
+
guard: (_, ctx) => ctx.activeRunId === undefined,
|
|
28
|
+
to: "failed" },
|
|
29
|
+
// ── PR lifecycle ───────────────────────────────────────────────
|
|
30
|
+
{ event: "pr_opened",
|
|
31
|
+
guard: (s) => s === "implementing",
|
|
32
|
+
to: "pr_open" },
|
|
33
|
+
// ── Review events — apply when no Codex run is actively executing ──
|
|
34
|
+
// Uses activeRunId (runtime state) rather than the state name, because
|
|
35
|
+
// states like changes_requested are "run states" only while the run is
|
|
36
|
+
// active — once the run completes, reviews should be accepted.
|
|
37
|
+
{ event: "review_approved",
|
|
38
|
+
guard: (s, ctx) => isOpen(s) && ctx.activeRunId === undefined,
|
|
39
|
+
to: "awaiting_queue" },
|
|
40
|
+
{ event: "review_changes_requested",
|
|
41
|
+
guard: (s, ctx) => isOpen(s) && ctx.activeRunId === undefined,
|
|
42
|
+
to: "changes_requested" },
|
|
43
|
+
// review_commented: no rule → no transition (informational only)
|
|
44
|
+
// ── CI check events ────────────────────────────────────────────
|
|
45
|
+
// After queue repair, return to the merge queue.
|
|
46
|
+
{ event: "check_passed",
|
|
47
|
+
guard: (s) => s === "repairing_queue",
|
|
48
|
+
to: "awaiting_queue" },
|
|
49
|
+
// After CI repair, return to merge queue if already approved,
|
|
50
|
+
// otherwise to pr_open for review.
|
|
51
|
+
{ event: "check_passed",
|
|
52
|
+
guard: (s) => s === "repairing_ci",
|
|
53
|
+
to: (_, ctx) => ctx.prReviewState === "approved" ? "awaiting_queue" : "pr_open" },
|
|
54
|
+
// CI failure when no run is active triggers repair.
|
|
55
|
+
{ event: "check_failed",
|
|
56
|
+
guard: (s, ctx) => isOpen(s) && ctx.activeRunId === undefined,
|
|
57
|
+
to: "repairing_ci" },
|
|
58
|
+
// ── Merge queue events ─────────────────────────────────────────
|
|
59
|
+
{ event: "merge_group_failed",
|
|
60
|
+
guard: (s) => s === "awaiting_queue",
|
|
61
|
+
to: "repairing_queue" },
|
|
62
|
+
// merge_group_passed: no rule → no transition (merge event follows)
|
|
63
|
+
// pr_synchronize: no rule → no transition (resets counters only)
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the next factory state from a GitHub webhook event.
|
|
67
|
+
*
|
|
68
|
+
* Returns `undefined` when no rule matches — the event is a no-op
|
|
69
|
+
* for the current state (e.g. check_passed while implementing).
|
|
70
|
+
*/
|
|
71
|
+
export function resolveFactoryStateFromGitHub(triggerEvent, current, ctx = {}) {
|
|
72
|
+
for (const rule of TRANSITION_RULES) {
|
|
73
|
+
if (rule.event !== triggerEvent)
|
|
74
|
+
continue;
|
|
75
|
+
if (rule.guard && !rule.guard(current, ctx))
|
|
76
|
+
continue;
|
|
77
|
+
return typeof rule.to === "function" ? rule.to(current, ctx) : rule.to;
|
|
60
78
|
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Derive the allowed transitions table from the rules for documentation
|
|
83
|
+
* and test validation. Not used at runtime.
|
|
84
|
+
*/
|
|
85
|
+
export function deriveAllowedTransitions(states, events) {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const state of states) {
|
|
88
|
+
result[state] = new Set();
|
|
89
|
+
}
|
|
90
|
+
// Sample with common review states to catch dynamic targets
|
|
91
|
+
const contexts = [
|
|
92
|
+
{},
|
|
93
|
+
{ prReviewState: "approved" },
|
|
94
|
+
{ prReviewState: "changes_requested" },
|
|
95
|
+
];
|
|
96
|
+
for (const state of states) {
|
|
97
|
+
for (const event of events) {
|
|
98
|
+
for (const ctx of contexts) {
|
|
99
|
+
const target = resolveFactoryStateFromGitHub(event, state, ctx);
|
|
100
|
+
if (target !== undefined && target !== state) {
|
|
101
|
+
result[state].add(target);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
61
107
|
}
|
|
@@ -15,25 +15,6 @@ function isMetadataOnlyCheckEvent(event) {
|
|
|
15
15
|
return event.eventSource === "check_run"
|
|
16
16
|
&& (event.triggerEvent === "check_passed" || event.triggerEvent === "check_failed");
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Codex sometimes closes and immediately reopens a PR (e.g. to change the
|
|
20
|
-
* base branch or fix the title). A pr_closed event during an active run
|
|
21
|
-
* should not transition to "failed" — the reopened event will follow.
|
|
22
|
-
* Without this guard, the state gets stuck at "failed" because
|
|
23
|
-
* failed → pr_open is not an allowed transition.
|
|
24
|
-
*/
|
|
25
|
-
function shouldSuppressCloseTransition(newState, event, issue) {
|
|
26
|
-
return newState === "failed" && event.triggerEvent === "pr_closed" && issue.activeRunId !== undefined;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* After a CI repair succeeds and CI passes, the resolver returns pr_open.
|
|
30
|
-
* If the PR is already approved, fast-track to awaiting_queue so the merge
|
|
31
|
-
* queue picks it up again. This avoids a dead state where the PR is approved
|
|
32
|
-
* and CI-green but nobody advances the merge queue.
|
|
33
|
-
*/
|
|
34
|
-
function shouldFastTrackToQueue(newState, issue) {
|
|
35
|
-
return newState === "pr_open" && issue.prReviewState === "approved";
|
|
36
|
-
}
|
|
37
18
|
export class GitHubWebhookHandler {
|
|
38
19
|
config;
|
|
39
20
|
db;
|
|
@@ -141,15 +122,12 @@ export class GitHubWebhookHandler {
|
|
|
141
122
|
...(event.checkStatus !== undefined ? { prCheckStatus: event.checkStatus } : {}),
|
|
142
123
|
});
|
|
143
124
|
if (!isMetadataOnlyCheckEvent(event)) {
|
|
144
|
-
// Re-read issue after PR metadata upsert so
|
|
125
|
+
// Re-read issue after PR metadata upsert so guards see fresh prReviewState
|
|
145
126
|
const afterMetadata = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (shouldFastTrackToQueue(newState, afterMetadata)) {
|
|
151
|
-
newState = "awaiting_queue";
|
|
152
|
-
}
|
|
127
|
+
const newState = resolveFactoryStateFromGitHub(event.triggerEvent, afterMetadata.factoryState, {
|
|
128
|
+
prReviewState: afterMetadata.prReviewState,
|
|
129
|
+
activeRunId: afterMetadata.activeRunId,
|
|
130
|
+
});
|
|
153
131
|
// Only transition and notify when the state actually changes.
|
|
154
132
|
// Multiple check_suite events can arrive for the same outcome.
|
|
155
133
|
if (newState && newState !== afterMetadata.factoryState) {
|