merge-steward 0.33.0 → 0.33.2
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/github/check-run-reporter.js +9 -1
- package/dist/github/pr-client.js +6 -4
- package/dist/github/shell-git.d.ts +1 -0
- package/dist/github/shell-git.js +7 -0
- package/dist/install.js +2 -14
- package/dist/interfaces.d.ts +1 -0
- package/dist/open-pr-ancestry.d.ts +25 -0
- package/dist/open-pr-ancestry.js +33 -0
- package/dist/reconciler-evict.d.ts +6 -0
- package/dist/reconciler-evict.js +1 -0
- package/dist/reconciler-merge.js +33 -0
- package/dist/reconciler-prepare.js +34 -0
- package/dist/reconciler.js +9 -2
- package/dist/types.d.ts +7 -1
- package/package.json +1 -1
|
@@ -88,7 +88,9 @@ function formatTitle(incident) {
|
|
|
88
88
|
case "main_broken":
|
|
89
89
|
return "Queue eviction: main branch CI failing";
|
|
90
90
|
case "policy_blocked":
|
|
91
|
-
return
|
|
91
|
+
return incident.context.openPrAncestors?.length
|
|
92
|
+
? "Queue eviction: candidate shares unlanded history with another open PR"
|
|
93
|
+
: "Queue eviction: approval withdrawn";
|
|
92
94
|
default:
|
|
93
95
|
return "Queue eviction";
|
|
94
96
|
}
|
|
@@ -114,6 +116,12 @@ function formatSummary(entry, incident) {
|
|
|
114
116
|
lines.push(`- ${c.name} (${c.conclusion})`);
|
|
115
117
|
}
|
|
116
118
|
}
|
|
119
|
+
if (incident.context.openPrAncestors?.length) {
|
|
120
|
+
lines.push(``, `**Unlanded open PR ancestry:**`);
|
|
121
|
+
for (const blocker of incident.context.openPrAncestors) {
|
|
122
|
+
lines.push(`- #${blocker.prNumber} ${blocker.branch} (head ${blocker.headSha}; shared unlanded ancestor ${blocker.sharedAncestorSha})`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
117
125
|
lines.push(``, `**Incident ID:** ${incident.id}`);
|
|
118
126
|
return lines.join("\n");
|
|
119
127
|
}
|
package/dist/github/pr-client.js
CHANGED
|
@@ -96,15 +96,17 @@ export class GitHubPRClient {
|
|
|
96
96
|
"--repo", this.repoFullName,
|
|
97
97
|
"--state", "open",
|
|
98
98
|
"--json", "number,headRefName,headRefOid",
|
|
99
|
+
"--limit", "1000",
|
|
99
100
|
], { allowNonZero: true, githubRepoFullName: this.repoFullName });
|
|
100
|
-
if (result.exitCode !== 0)
|
|
101
|
-
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
throw new Error(`Failed to list open PRs: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
103
|
+
}
|
|
102
104
|
try {
|
|
103
105
|
const data = JSON.parse(result.stdout);
|
|
104
106
|
return data.map((pr) => ({ number: pr.number, branch: pr.headRefName, headSha: pr.headRefOid }));
|
|
105
107
|
}
|
|
106
|
-
catch {
|
|
107
|
-
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error("GitHub returned malformed open PR data", { cause: error });
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
async deleteBranch(prNumber) {
|
|
@@ -23,6 +23,7 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
|
|
|
23
23
|
private gitIn;
|
|
24
24
|
fetch(remote?: string): Promise<void>;
|
|
25
25
|
headSha(branch: string): Promise<string>;
|
|
26
|
+
mergeBase(left: string, right: string): Promise<string>;
|
|
26
27
|
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
27
28
|
push(branch: string, force?: boolean, targetBranch?: string): Promise<void>;
|
|
28
29
|
/**
|
package/dist/github/shell-git.js
CHANGED
|
@@ -74,6 +74,13 @@ export class ShellGitOperations {
|
|
|
74
74
|
const result = await this.git(["rev-parse", branch]);
|
|
75
75
|
return result.stdout.trim();
|
|
76
76
|
}
|
|
77
|
+
async mergeBase(left, right) {
|
|
78
|
+
const result = await this.git(["merge-base", left, right]);
|
|
79
|
+
const sha = result.stdout.trim();
|
|
80
|
+
if (!sha)
|
|
81
|
+
throw new Error(`git merge-base returned no commit for ${left} and ${right}`);
|
|
82
|
+
return sha;
|
|
83
|
+
}
|
|
77
84
|
async isAncestor(ancestor, descendant) {
|
|
78
85
|
const result = await this.git(["merge-base", "--is-ancestor", ancestor, descendant], { allowNonZero: true });
|
|
79
86
|
if (result.exitCode === 0)
|
package/dist/install.js
CHANGED
|
@@ -3,23 +3,12 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { DEFAULT_MERGE_QUEUE_CHECK_NAME, parseConfig } from "./config.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getDefaultConfigPath, getDefaultRuntimeEnvPath, getDefaultServiceEnvPath, getDefaultStateDir, getMergeStewardPathLayout, getRepoConfigPath, getSystemdUnitPath, readBundledAsset } from "./runtime-paths.js";
|
|
7
7
|
import { parseHomeConfigObject, stringifyJson } from "./steward-home.js";
|
|
8
8
|
function renderTemplate(template, replacements) {
|
|
9
9
|
const home = homedir();
|
|
10
10
|
const user = basename(home);
|
|
11
11
|
const layout = getMergeStewardPathLayout();
|
|
12
|
-
// When running from source (e.g. during tests or `npm run dev`) the dist/
|
|
13
|
-
// entrypoint isn't built yet. Fall back to the PATH-resolved binary so the
|
|
14
|
-
// systemd template still renders — the ExecStart will need `merge-steward`
|
|
15
|
-
// on PATH at service start, which is true for any normal install.
|
|
16
|
-
let cliEntry;
|
|
17
|
-
try {
|
|
18
|
-
cliEntry = getBuiltCliEntryPath();
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
cliEntry = "/usr/bin/env merge-steward";
|
|
22
|
-
}
|
|
23
12
|
let rendered = template
|
|
24
13
|
.replaceAll("/home/your-user", home)
|
|
25
14
|
.replaceAll("your-user", user)
|
|
@@ -29,8 +18,7 @@ function renderTemplate(template, replacements) {
|
|
|
29
18
|
.replaceAll("/home/your-user/.config/merge-steward/merge-steward.json", layout.configPath)
|
|
30
19
|
.replaceAll("/home/your-user/.config/merge-steward/repos", layout.repoConfigDir)
|
|
31
20
|
.replaceAll("/home/your-user/.local/state/merge-steward", layout.stateDir)
|
|
32
|
-
.replaceAll("/home/your-user/.local/share/merge-steward", layout.dataDir)
|
|
33
|
-
.replaceAll("/usr/bin/env merge-steward", cliEntry);
|
|
21
|
+
.replaceAll("/home/your-user/.local/share/merge-steward", layout.dataDir);
|
|
34
22
|
if (replacements?.publicBaseUrl) {
|
|
35
23
|
rendered = rendered.replaceAll("https://queue.example.com", replacements.publicBaseUrl);
|
|
36
24
|
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { CIStatus, CheckResult, IncidentRecord, MergeResult, PRStatus, Queu
|
|
|
6
6
|
export interface GitOperations {
|
|
7
7
|
fetch(remote?: string): Promise<void>;
|
|
8
8
|
headSha(branch: string): Promise<string>;
|
|
9
|
+
mergeBase(left: string, right: string): Promise<string>;
|
|
9
10
|
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
10
11
|
push(branch: string, force?: boolean, targetBranch?: string): Promise<void>;
|
|
11
12
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GitHubPRApi, GitOperations } from "./interfaces.ts";
|
|
2
|
+
export interface OpenPrAncestor {
|
|
3
|
+
prNumber: number;
|
|
4
|
+
branch: string;
|
|
5
|
+
headSha: string;
|
|
6
|
+
sharedAncestorSha: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Find ancestry shared with open PRs outside the current base.
|
|
10
|
+
*
|
|
11
|
+
* Testing only the other PR's current head is insufficient: a parent may have
|
|
12
|
+
* advanced after the child copied its earlier blocked head. The merge base
|
|
13
|
+
* captures that shared history. If it is absent from current main, landing the
|
|
14
|
+
* candidate can bypass the other PR's review gates. Explicit stacks become
|
|
15
|
+
* valid only after their shared parent history has landed in main.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findUnlandedOpenPrAncestors(params: {
|
|
18
|
+
github: GitHubPRApi;
|
|
19
|
+
git: GitOperations;
|
|
20
|
+
currentPrNumber: number;
|
|
21
|
+
prHeadSha: string;
|
|
22
|
+
candidateSha: string;
|
|
23
|
+
baseSha: string;
|
|
24
|
+
}): Promise<OpenPrAncestor[]>;
|
|
25
|
+
export declare function describeOpenPrAncestors(blockers: OpenPrAncestor[]): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find ancestry shared with open PRs outside the current base.
|
|
3
|
+
*
|
|
4
|
+
* Testing only the other PR's current head is insufficient: a parent may have
|
|
5
|
+
* advanced after the child copied its earlier blocked head. The merge base
|
|
6
|
+
* captures that shared history. If it is absent from current main, landing the
|
|
7
|
+
* candidate can bypass the other PR's review gates. Explicit stacks become
|
|
8
|
+
* valid only after their shared parent history has landed in main.
|
|
9
|
+
*/
|
|
10
|
+
export async function findUnlandedOpenPrAncestors(params) {
|
|
11
|
+
const openPrs = await params.github.listOpenPRs();
|
|
12
|
+
const blockers = [];
|
|
13
|
+
for (const openPr of openPrs) {
|
|
14
|
+
if (openPr.number === params.currentPrNumber)
|
|
15
|
+
continue;
|
|
16
|
+
if (openPr.headSha !== params.prHeadSha
|
|
17
|
+
&& await params.git.isAncestor(params.prHeadSha, openPr.headSha))
|
|
18
|
+
continue;
|
|
19
|
+
const sharedAncestorSha = await params.git.mergeBase(openPr.headSha, params.candidateSha);
|
|
20
|
+
if (await params.git.isAncestor(sharedAncestorSha, params.baseSha))
|
|
21
|
+
continue;
|
|
22
|
+
blockers.push({
|
|
23
|
+
prNumber: openPr.number,
|
|
24
|
+
branch: openPr.branch,
|
|
25
|
+
headSha: openPr.headSha,
|
|
26
|
+
sharedAncestorSha,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return blockers;
|
|
30
|
+
}
|
|
31
|
+
export function describeOpenPrAncestors(blockers) {
|
|
32
|
+
return `candidate shares unlanded history with open PR ${blockers.map((blocker) => `#${blocker.prNumber} (${blocker.sharedAncestorSha.slice(0, 12)})`).join(", ")}`;
|
|
33
|
+
}
|
|
@@ -9,4 +9,10 @@ export declare function evictEntry(ctx: ReconcileContext, entry: QueueEntry, fai
|
|
|
9
9
|
conclusion: string;
|
|
10
10
|
url?: string;
|
|
11
11
|
}>;
|
|
12
|
+
openPrAncestors?: Array<{
|
|
13
|
+
prNumber: number;
|
|
14
|
+
branch: string;
|
|
15
|
+
headSha: string;
|
|
16
|
+
sharedAncestorSha: string;
|
|
17
|
+
}>;
|
|
12
18
|
}): Promise<void>;
|
package/dist/reconciler-evict.js
CHANGED
|
@@ -59,6 +59,7 @@ export async function evictEntry(ctx, entry, failureClass, extra) {
|
|
|
59
59
|
queuePosition: entry.position,
|
|
60
60
|
conflictFiles: extra?.conflictFiles,
|
|
61
61
|
failedChecks: extra?.failedChecks,
|
|
62
|
+
openPrAncestors: extra?.openPrAncestors,
|
|
62
63
|
baseBranch: ctx.baseBranch,
|
|
63
64
|
branch: entry.branch,
|
|
64
65
|
issueKey: entry.issueKey,
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CLEAN_CANDIDATE_REF, CLEAN_CI, CLEAR_CANDIDATE, emit, isBudgetExhausted
|
|
|
2
2
|
import { cleanupCandidate, evictEntry, invalidateDownstream } from "./reconciler-evict.js";
|
|
3
3
|
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
4
4
|
import { evaluateCheckPolicy, formatRequiredCheck } from "./check-policy.js";
|
|
5
|
+
import { describeOpenPrAncestors, findUnlandedOpenPrAncestors } from "./open-pr-ancestry.js";
|
|
5
6
|
const DEFAULT_PR_MERGED_POLL_ATTEMPTS = 6;
|
|
6
7
|
const DEFAULT_PR_MERGED_POLL_DELAY_MS = 2_000;
|
|
7
8
|
function delay(ms) {
|
|
@@ -276,6 +277,38 @@ export async function mergeHead(ctx, entry) {
|
|
|
276
277
|
candidatePolicyFingerprint: currentPolicyFingerprint,
|
|
277
278
|
}, `candidate revalidated under policy ${currentPolicyFingerprint.slice(0, 12)}`);
|
|
278
279
|
}
|
|
280
|
+
// Ancestry is the last remote truth read before the immutable push. Fetch
|
|
281
|
+
// main again because check and PR revalidation above can take long enough
|
|
282
|
+
// for either main or the open-PR set to change.
|
|
283
|
+
await ctx.git.fetch();
|
|
284
|
+
currentBase = await ctx.git.headSha(ref(ctx, ctx.baseBranch));
|
|
285
|
+
if (!await ctx.git.isAncestor(currentBase, landingSha)) {
|
|
286
|
+
emit(ctx, entry, "branch_mismatch", {
|
|
287
|
+
detail: `candidate is not a fast-forward from final main (${currentBase.slice(0, 8)})`,
|
|
288
|
+
});
|
|
289
|
+
const allActive = ctx.store.listActive(ctx.repoId);
|
|
290
|
+
ctx.store.transition(entry.id, "preparing_head", { ...CLEAN_CI, ...CLEAR_CANDIDATE }, "main changed during landing, re-prepare");
|
|
291
|
+
await invalidateDownstream(ctx, allActive, 0);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const ancestryBlockers = await findUnlandedOpenPrAncestors({
|
|
295
|
+
github: ctx.github,
|
|
296
|
+
git: ctx.git,
|
|
297
|
+
currentPrNumber: entry.prNumber,
|
|
298
|
+
prHeadSha: entry.headSha,
|
|
299
|
+
candidateSha: landingSha,
|
|
300
|
+
baseSha: currentBase,
|
|
301
|
+
});
|
|
302
|
+
if (ancestryBlockers.length > 0) {
|
|
303
|
+
const detail = describeOpenPrAncestors(ancestryBlockers);
|
|
304
|
+
emit(ctx, entry, "open_pr_ancestry_blocked", { baseSha: currentBase, detail });
|
|
305
|
+
const allActive = ctx.store.listActive(ctx.repoId);
|
|
306
|
+
const entryIndex = allActive.findIndex((candidate) => candidate.id === entry.id);
|
|
307
|
+
await evictEntry(ctx, entry, "policy_blocked", { openPrAncestors: ancestryBlockers });
|
|
308
|
+
if (entryIndex >= 0)
|
|
309
|
+
await invalidateDownstream(ctx, allActive, entryIndex);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
279
312
|
}
|
|
280
313
|
catch (error) {
|
|
281
314
|
const detail = `candidate revalidation unavailable: ${describeError(error)}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CLEAN_CI, CLEAR_CANDIDATE, emit, isRetryGated, ref, candidateRefName } from "./reconciler-core.js";
|
|
2
2
|
import { evictEntry } from "./reconciler-evict.js";
|
|
3
|
+
import { describeOpenPrAncestors, findUnlandedOpenPrAncestors } from "./open-pr-ancestry.js";
|
|
3
4
|
export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
4
5
|
emit(ctx, entry, "fetch_started");
|
|
5
6
|
await ctx.git.fetch();
|
|
@@ -13,6 +14,22 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
13
14
|
ctx.store.updateHead(entry.id, currentRef);
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
17
|
+
if (isHead) {
|
|
18
|
+
const blockers = await findUnlandedOpenPrAncestors({
|
|
19
|
+
github: ctx.github,
|
|
20
|
+
git: ctx.git,
|
|
21
|
+
currentPrNumber: entry.prNumber,
|
|
22
|
+
prHeadSha: entry.headSha,
|
|
23
|
+
candidateSha: entry.headSha,
|
|
24
|
+
baseSha,
|
|
25
|
+
});
|
|
26
|
+
if (blockers.length > 0) {
|
|
27
|
+
const detail = describeOpenPrAncestors(blockers);
|
|
28
|
+
emit(ctx, entry, "open_pr_ancestry_blocked", { baseSha, detail });
|
|
29
|
+
await evictEntry(ctx, entry, "policy_blocked", { openPrAncestors: blockers });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
16
33
|
// Preparing/validating the head never waits on main's CI. The exact
|
|
17
34
|
// candidate includes current main and is gated solely by its own checks.
|
|
18
35
|
//
|
|
@@ -34,6 +51,23 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
34
51
|
}
|
|
35
52
|
return;
|
|
36
53
|
}
|
|
54
|
+
// A remote merge can advance main before GitHub's PR API reports `merged`.
|
|
55
|
+
// The PR head is then already contained in main, so building and testing an
|
|
56
|
+
// "integration" candidate would only reproduce the current main commit.
|
|
57
|
+
// Hold the entry until sanitizeEntry observes GitHub's terminal truth.
|
|
58
|
+
if (baseSha !== entry.headSha && await ctx.git.isAncestor(entry.headSha, baseSha)) {
|
|
59
|
+
const detail = `PR head ${entry.headSha.slice(0, 12)} is already contained in main ${baseSha.slice(0, 12)}; waiting for GitHub merge recognition`;
|
|
60
|
+
if (entry.waitDetail !== detail) {
|
|
61
|
+
emit(ctx, entry, "merge_waiting_recognition", { baseSha, detail });
|
|
62
|
+
ctx.store.transition(entry.id, "preparing_head", {
|
|
63
|
+
baseSha,
|
|
64
|
+
...CLEAN_CI,
|
|
65
|
+
...CLEAR_CANDIDATE,
|
|
66
|
+
waitDetail: detail,
|
|
67
|
+
}, detail);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
37
71
|
// Candidate selection is structural, not a separate fast-forward workflow.
|
|
38
72
|
// If the prospective base is already an ancestor of the PR head, that exact
|
|
39
73
|
// immutable head is the future main candidate and its existing SHA-bound
|
package/dist/reconciler.js
CHANGED
|
@@ -48,11 +48,12 @@ export async function reconcile(ctx) {
|
|
|
48
48
|
// Truth guard: verify entry against GitHub before processing.
|
|
49
49
|
if (await sanitizeEntry(ctx, entry)) {
|
|
50
50
|
// Entry may have been terminalized (closed/duplicate) — clear any
|
|
51
|
-
// stale queue sub-state label it still carries.
|
|
51
|
+
// stale queue sub-state label it still carries. Stop this snapshot:
|
|
52
|
+
// later entries were ordered against topology that has now changed.
|
|
52
53
|
const sanitized = ctx.store.getEntry(entry.id);
|
|
53
54
|
if (sanitized)
|
|
54
55
|
await syncQueueStateLabels(ctx, sanitized);
|
|
55
|
-
|
|
56
|
+
break;
|
|
56
57
|
}
|
|
57
58
|
// Stale dependency guard: if this entry's spec was built on top of
|
|
58
59
|
// another entry that was dequeued or evicted (without downstream
|
|
@@ -112,6 +113,12 @@ export async function reconcile(ctx) {
|
|
|
112
113
|
const post = ctx.store.getEntry(entry.id);
|
|
113
114
|
if (post)
|
|
114
115
|
await syncQueueStateLabels(ctx, post);
|
|
116
|
+
if (post && (post.status === "evicted" || post.status === "dequeued")) {
|
|
117
|
+
// Dependency readiness was computed before this terminal transition.
|
|
118
|
+
// Re-enter on the next tick so an independent root cannot reuse or
|
|
119
|
+
// rebuild through a child that just became blocked by its parent.
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
await verifyMergedEntriesPostPush(ctx);
|
|
117
124
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ export interface EvictionContext {
|
|
|
103
103
|
conclusion: string;
|
|
104
104
|
url?: string;
|
|
105
105
|
}> | undefined;
|
|
106
|
+
openPrAncestors?: Array<{
|
|
107
|
+
prNumber: number;
|
|
108
|
+
branch: string;
|
|
109
|
+
headSha: string;
|
|
110
|
+
sharedAncestorSha: string;
|
|
111
|
+
}> | undefined;
|
|
106
112
|
baseSha: string;
|
|
107
113
|
prHeadSha: string;
|
|
108
114
|
queuePosition: number;
|
|
@@ -240,7 +246,7 @@ export interface QueueConfig {
|
|
|
240
246
|
flakyRetries: number;
|
|
241
247
|
pollIntervalMs: number;
|
|
242
248
|
}
|
|
243
|
-
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "candidate_selected" | "integration_build_started" | "integration_build_succeeded" | "integration_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "head_candidate_landed" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "stack_dependency_waiting" | "merge_waiting_main" | "main_pending_bypassed" | "pr_branch_cleanup_deferred" | "pr_branch_cleanup_failed" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "queue_label_synced" | "branch_unreachable";
|
|
249
|
+
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "candidate_selected" | "integration_build_started" | "integration_build_succeeded" | "integration_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "head_candidate_landed" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "open_pr_ancestry_blocked" | "stack_dependency_waiting" | "merge_waiting_recognition" | "merge_waiting_main" | "main_pending_bypassed" | "pr_branch_cleanup_deferred" | "pr_branch_cleanup_failed" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "queue_label_synced" | "branch_unreachable";
|
|
244
250
|
export interface ReconcileEvent {
|
|
245
251
|
at: string;
|
|
246
252
|
entryId: string;
|