merge-steward 0.24.0 → 0.25.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/db/schema.js +3 -0
- package/dist/db/sqlite-store.d.ts +9 -1
- package/dist/db/sqlite-store.js +33 -0
- package/dist/github/shell-git.d.ts +5 -0
- package/dist/github/shell-git.js +62 -0
- package/dist/interfaces.d.ts +10 -0
- package/dist/reconciler-prepare.js +101 -0
- package/dist/service-queue.js +2 -0
- package/dist/store.d.ts +19 -1
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/dist/db/schema.js
CHANGED
|
@@ -76,6 +76,9 @@ export function ensureSchema(connection) {
|
|
|
76
76
|
ensureColumn(connection, "queue_entries", "post_merge_checked_at", "TEXT");
|
|
77
77
|
ensureColumn(connection, "queue_entries", "wait_detail", "TEXT");
|
|
78
78
|
ensureColumn(connection, "queue_entries", "pr_title", "TEXT");
|
|
79
|
+
// Plan §5.3: cached identity for patch-id-aware updateHead.
|
|
80
|
+
ensureColumn(connection, "queue_entries", "head_patch_id", "TEXT");
|
|
81
|
+
ensureColumn(connection, "queue_entries", "spec_tree_id", "TEXT");
|
|
79
82
|
connection.exec(`
|
|
80
83
|
UPDATE queue_entries
|
|
81
84
|
SET post_merge_status = 'pending'
|
|
@@ -12,9 +12,17 @@ export declare class SqliteStore implements QueueStore {
|
|
|
12
12
|
listActive(repoId: string): QueueEntry[];
|
|
13
13
|
listAll(repoId: string): QueueEntry[];
|
|
14
14
|
insert(entry: QueueEntry): void;
|
|
15
|
-
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
15
|
+
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt" | "headPatchId" | "specTreeId">>, detail?: string): void;
|
|
16
16
|
dequeue(entryId: string): void;
|
|
17
17
|
updateHead(entryId: string, newHeadSha: string): void;
|
|
18
|
+
rebuildSpecHeadEquivalent(entryId: string, patch: {
|
|
19
|
+
headSha: string;
|
|
20
|
+
specSha: string;
|
|
21
|
+
specBranch: string;
|
|
22
|
+
headPatchId: string;
|
|
23
|
+
specTreeId: string;
|
|
24
|
+
ciRunId: string | null;
|
|
25
|
+
}, detail?: string): void;
|
|
18
26
|
updatePriority(entryId: string, priority: number, detail?: string): void;
|
|
19
27
|
insertIncident(incident: IncidentRecord): void;
|
|
20
28
|
listIncidents(entryId: string): IncidentRecord[];
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -30,6 +30,8 @@ function mapEntry(row) {
|
|
|
30
30
|
postMergeSummary: row.post_merge_summary === null || row.post_merge_summary === undefined ? null : String(row.post_merge_summary),
|
|
31
31
|
postMergeCheckedAt: row.post_merge_checked_at === null || row.post_merge_checked_at === undefined ? null : String(row.post_merge_checked_at),
|
|
32
32
|
prTitle: row.pr_title === null || row.pr_title === undefined ? null : String(row.pr_title),
|
|
33
|
+
headPatchId: row.head_patch_id === null || row.head_patch_id === undefined ? null : String(row.head_patch_id),
|
|
34
|
+
specTreeId: row.spec_tree_id === null || row.spec_tree_id === undefined ? null : String(row.spec_tree_id),
|
|
33
35
|
enqueuedAt: String(row.enqueued_at),
|
|
34
36
|
updatedAt: String(row.updated_at),
|
|
35
37
|
};
|
|
@@ -199,6 +201,14 @@ export class SqliteStore {
|
|
|
199
201
|
sets.push("post_merge_checked_at = ?");
|
|
200
202
|
values.push(patch.postMergeCheckedAt);
|
|
201
203
|
}
|
|
204
|
+
if (patch?.headPatchId !== undefined) {
|
|
205
|
+
sets.push("head_patch_id = ?");
|
|
206
|
+
values.push(patch.headPatchId);
|
|
207
|
+
}
|
|
208
|
+
if (patch?.specTreeId !== undefined) {
|
|
209
|
+
sets.push("spec_tree_id = ?");
|
|
210
|
+
values.push(patch.specTreeId);
|
|
211
|
+
}
|
|
202
212
|
this.conn.prepare(`UPDATE queue_entries SET ${sets.join(", ")} WHERE id = ?`).run(...values, entryId);
|
|
203
213
|
this.writeEvent(entryId, from, to, detail);
|
|
204
214
|
})();
|
|
@@ -222,11 +232,34 @@ export class SqliteStore {
|
|
|
222
232
|
spec_branch = NULL, spec_sha = NULL, spec_based_on = NULL,
|
|
223
233
|
wait_detail = NULL,
|
|
224
234
|
post_merge_status = NULL, post_merge_sha = NULL, post_merge_summary = NULL, post_merge_checked_at = NULL,
|
|
235
|
+
head_patch_id = NULL, spec_tree_id = NULL,
|
|
225
236
|
updated_at = ?
|
|
226
237
|
WHERE id = ?`).run(newHeadSha, newGen, isoNow(), entryId);
|
|
227
238
|
this.writeEvent(entryId, from, "queued", `updateHead: generation ${newGen}`);
|
|
228
239
|
})();
|
|
229
240
|
}
|
|
241
|
+
rebuildSpecHeadEquivalent(entryId, patch, detail) {
|
|
242
|
+
this.conn.transaction(() => {
|
|
243
|
+
const current = this.conn.prepare("SELECT status, generation FROM queue_entries WHERE id = ?").get(entryId);
|
|
244
|
+
if (!current)
|
|
245
|
+
return;
|
|
246
|
+
const from = String(current.status);
|
|
247
|
+
if (TERMINAL_STATUSES.includes(from))
|
|
248
|
+
return;
|
|
249
|
+
const newGen = Number(current.generation) + 1;
|
|
250
|
+
this.conn.prepare(`UPDATE queue_entries SET
|
|
251
|
+
head_sha = ?, status = 'validating', generation = ?,
|
|
252
|
+
ci_run_id = ?, ci_retries = 0, retry_attempts = 0,
|
|
253
|
+
last_failed_base_sha = NULL,
|
|
254
|
+
spec_branch = ?, spec_sha = ?,
|
|
255
|
+
head_patch_id = ?, spec_tree_id = ?,
|
|
256
|
+
wait_detail = NULL,
|
|
257
|
+
post_merge_status = NULL, post_merge_sha = NULL, post_merge_summary = NULL, post_merge_checked_at = NULL,
|
|
258
|
+
updated_at = ?
|
|
259
|
+
WHERE id = ?`).run(patch.headSha, newGen, patch.ciRunId, patch.specBranch, patch.specSha, patch.headPatchId, patch.specTreeId, isoNow(), entryId);
|
|
260
|
+
this.writeEvent(entryId, from, "validating", detail ?? `patch-id-equivalent rebuild: generation ${newGen}`);
|
|
261
|
+
})();
|
|
262
|
+
}
|
|
230
263
|
updatePriority(entryId, priority, detail) {
|
|
231
264
|
this.conn.transaction(() => {
|
|
232
265
|
const current = this.conn.prepare("SELECT status, priority FROM queue_entries WHERE id = ?").get(entryId);
|
|
@@ -25,6 +25,11 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
|
|
|
25
25
|
headSha(branch: string): Promise<string>;
|
|
26
26
|
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
27
27
|
push(branch: string, force?: boolean, targetBranch?: string): Promise<void>;
|
|
28
|
+
patchIdAgainst(base: string, headSha: string): Promise<string | undefined>;
|
|
29
|
+
integrationTreeId(base: string, headSha: string): Promise<string | undefined>;
|
|
30
|
+
treeId(commitSha: string): Promise<string | undefined>;
|
|
31
|
+
commitTree(tree: string, parents: string[], message: string): Promise<string | undefined>;
|
|
32
|
+
pushCommit(commitSha: string, branch: string): Promise<void>;
|
|
28
33
|
/**
|
|
29
34
|
* Build a speculative merge branch using an isolated git worktree.
|
|
30
35
|
* Each call gets its own working directory — no shared mutable state.
|
package/dist/github/shell-git.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { exec } from "../exec.js";
|
|
4
|
+
/** Shell-quote a string for sh -c invocation (single-quote, escape inner quotes). */
|
|
5
|
+
function shellQuote(value) {
|
|
6
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
7
|
+
}
|
|
4
8
|
/** Extract conflict file names from git merge stderr. */
|
|
5
9
|
function parseConflicts(stderr) {
|
|
6
10
|
const files = stderr
|
|
@@ -90,6 +94,64 @@ export class ShellGitOperations {
|
|
|
90
94
|
args.push("origin", refspec);
|
|
91
95
|
await this.git(args, { timeoutMs: 60_000 });
|
|
92
96
|
}
|
|
97
|
+
// Plan §5.3: identity primitives for patch-id-aware updateHead.
|
|
98
|
+
async patchIdAgainst(base, headSha) {
|
|
99
|
+
// `git diff base..head | git patch-id --stable` — invoke as one
|
|
100
|
+
// pipeline through a shell so we don't have to plumb stdin/stdout.
|
|
101
|
+
const result = await exec("sh", ["-c", `${this.gitBin} -C ${shellQuote(this.clonePath)} diff ${shellQuote(base)}..${shellQuote(headSha)} | ${this.gitBin} patch-id --stable`], {
|
|
102
|
+
timeoutMs: 30_000,
|
|
103
|
+
allowNonZero: true,
|
|
104
|
+
githubRepoFullName: this.repoFullName,
|
|
105
|
+
});
|
|
106
|
+
if (result.exitCode !== 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
const first = result.stdout.split(/\s+/, 1)[0]?.trim();
|
|
109
|
+
return first ? first : undefined;
|
|
110
|
+
}
|
|
111
|
+
async integrationTreeId(base, headSha) {
|
|
112
|
+
const result = await this.git(["merge-tree", "--write-tree", "--no-messages", base, headSha], { allowNonZero: true, timeoutMs: 60_000 });
|
|
113
|
+
if (result.exitCode !== 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
const tree = result.stdout.trim().split(/\s+/, 1)[0];
|
|
116
|
+
return tree || undefined;
|
|
117
|
+
}
|
|
118
|
+
async treeId(commitSha) {
|
|
119
|
+
const result = await this.git(["rev-parse", `${commitSha}^{tree}`], { allowNonZero: true });
|
|
120
|
+
if (result.exitCode !== 0)
|
|
121
|
+
return undefined;
|
|
122
|
+
const tree = result.stdout.trim();
|
|
123
|
+
return tree || undefined;
|
|
124
|
+
}
|
|
125
|
+
async commitTree(tree, parents, message) {
|
|
126
|
+
const args = ["commit-tree", tree];
|
|
127
|
+
for (const parent of parents) {
|
|
128
|
+
args.push("-p", parent);
|
|
129
|
+
}
|
|
130
|
+
args.push("-m", message);
|
|
131
|
+
const env = {};
|
|
132
|
+
if (this.botIdentity) {
|
|
133
|
+
env.GIT_AUTHOR_NAME = this.botIdentity.name;
|
|
134
|
+
env.GIT_AUTHOR_EMAIL = this.botIdentity.email;
|
|
135
|
+
env.GIT_COMMITTER_NAME = this.botIdentity.name;
|
|
136
|
+
env.GIT_COMMITTER_EMAIL = this.botIdentity.email;
|
|
137
|
+
}
|
|
138
|
+
const result = await exec(this.gitBin, ["-C", this.clonePath, ...args], {
|
|
139
|
+
timeoutMs: 30_000,
|
|
140
|
+
allowNonZero: true,
|
|
141
|
+
githubRepoFullName: this.repoFullName,
|
|
142
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
143
|
+
});
|
|
144
|
+
if (result.exitCode !== 0)
|
|
145
|
+
return undefined;
|
|
146
|
+
const sha = result.stdout.trim();
|
|
147
|
+
return sha || undefined;
|
|
148
|
+
}
|
|
149
|
+
async pushCommit(commitSha, branch) {
|
|
150
|
+
// Force-update the named branch on origin to a known commit
|
|
151
|
+
// SHA. Used by the patch-id-aware rebuild path which writes a
|
|
152
|
+
// synthetic commit-tree result without checking it out.
|
|
153
|
+
await this.git(["push", "--force", "origin", `${commitSha}:refs/heads/${branch}`], { timeoutMs: 60_000 });
|
|
154
|
+
}
|
|
93
155
|
// ─── SpeculativeBranchBuilder ───────────────────────────────
|
|
94
156
|
/**
|
|
95
157
|
* Build a speculative merge branch using an isolated git worktree.
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export interface GitOperations {
|
|
|
8
8
|
headSha(branch: string): Promise<string>;
|
|
9
9
|
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
10
10
|
push(branch: string, force?: boolean, targetBranch?: string): Promise<void>;
|
|
11
|
+
/** Stable patch-id of head's diff against base. Returns undefined on git error. */
|
|
12
|
+
patchIdAgainst?(base: string, headSha: string): Promise<string | undefined>;
|
|
13
|
+
/** `git merge-tree --write-tree base headSha` — returns tree-id, or undefined on conflict/error. */
|
|
14
|
+
integrationTreeId?(base: string, headSha: string): Promise<string | undefined>;
|
|
15
|
+
/** Tree id of a commit (`commit^{tree}`). */
|
|
16
|
+
treeId?(commitSha: string): Promise<string | undefined>;
|
|
17
|
+
/** `git commit-tree tree -p p1 -p p2 -m message`. Returns the new commit SHA. */
|
|
18
|
+
commitTree?(tree: string, parents: string[], message: string): Promise<string | undefined>;
|
|
19
|
+
/** Force-push a known commit SHA to a branch (overrides the target ref directly). */
|
|
20
|
+
pushCommit?(commitSha: string, branch: string): Promise<void>;
|
|
11
21
|
}
|
|
12
22
|
/**
|
|
13
23
|
* Builds and manages speculative cumulative branches.
|
|
@@ -41,6 +41,18 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
41
41
|
const currentRef = await ctx.git.headSha(ref(ctx, entry.branch));
|
|
42
42
|
if (currentRef !== entry.headSha) {
|
|
43
43
|
emit(ctx, entry, "branch_mismatch", { detail: `expected ${entry.headSha.slice(0, 8)}, got ${currentRef.slice(0, 8)}` });
|
|
44
|
+
// Plan §5.3: patch-id-aware short-circuit. If the new head is
|
|
45
|
+
// patch-id-equivalent to the prior head AND the freshly merged
|
|
46
|
+
// tree matches the cached spec tree, rebuild the spec commit
|
|
47
|
+
// with the new head as its second parent so GitHub still marks
|
|
48
|
+
// the PR merged after the lander's fast-forward — but skip the
|
|
49
|
+
// prepare cycle. CI must re-run on the new spec SHA (check_runs
|
|
50
|
+
// are SHA-anchored). Falls back to the standard `updateHead`
|
|
51
|
+
// path if the git plumbing is unavailable or if the identities
|
|
52
|
+
// don't match.
|
|
53
|
+
const shortCircuited = await tryPatchIdEquivalentRebuild(ctx, entry, base, currentRef);
|
|
54
|
+
if (shortCircuited)
|
|
55
|
+
return;
|
|
44
56
|
ctx.store.updateHead(entry.id, currentRef);
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
@@ -170,6 +182,16 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
170
182
|
}
|
|
171
183
|
const runId = await ctx.ci.triggerRun(specName, specSha);
|
|
172
184
|
emit(ctx, entry, "ci_triggered", { ciRunId: runId, specBranch: specName });
|
|
185
|
+
// Plan §5.3: cache identity for the patch-id-aware short-circuit
|
|
186
|
+
// on subsequent updateHead detections. Best-effort — if the git
|
|
187
|
+
// primitives are missing the cached identity stays null and the
|
|
188
|
+
// short-circuit never fires (existing behavior).
|
|
189
|
+
const headPatchId = ctx.git.patchIdAgainst
|
|
190
|
+
? await ctx.git.patchIdAgainst(base, entry.headSha).catch(() => undefined)
|
|
191
|
+
: undefined;
|
|
192
|
+
const specTreeId = ctx.git.treeId
|
|
193
|
+
? await ctx.git.treeId(specSha).catch(() => undefined)
|
|
194
|
+
: undefined;
|
|
173
195
|
ctx.store.transition(entry.id, "validating", {
|
|
174
196
|
baseSha,
|
|
175
197
|
ciRunId: runId,
|
|
@@ -177,5 +199,84 @@ export async function prepareEntry(ctx, entry, isHead, prevEntry) {
|
|
|
177
199
|
specBranch: specName,
|
|
178
200
|
specSha,
|
|
179
201
|
specBasedOn: isHead ? null : prevEntry.id,
|
|
202
|
+
...(headPatchId !== undefined ? { headPatchId } : {}),
|
|
203
|
+
...(specTreeId !== undefined ? { specTreeId } : {}),
|
|
180
204
|
}, `spec ready, CI ${runId.slice(0, 12)}`);
|
|
181
205
|
}
|
|
206
|
+
async function tryPatchIdEquivalentRebuild(ctx, entry, base, newHead) {
|
|
207
|
+
// Need the cached identity *and* the git primitives required to
|
|
208
|
+
// verify the equivalence and rebuild the spec commit. If any
|
|
209
|
+
// piece is missing, fall back.
|
|
210
|
+
if (!entry.headPatchId || !entry.specTreeId || !entry.specBranch || !entry.specSha) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const git = ctx.git;
|
|
214
|
+
if (!git.patchIdAgainst || !git.integrationTreeId || !git.commitTree || !git.pushCommit) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
let newPatchId;
|
|
218
|
+
let newTreeId;
|
|
219
|
+
let baseSha;
|
|
220
|
+
try {
|
|
221
|
+
[newPatchId, newTreeId, baseSha] = await Promise.all([
|
|
222
|
+
git.patchIdAgainst(base, newHead),
|
|
223
|
+
git.integrationTreeId(base, newHead),
|
|
224
|
+
git.headSha(base),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (!newPatchId || !newTreeId || !baseSha)
|
|
231
|
+
return false;
|
|
232
|
+
if (newPatchId !== entry.headPatchId)
|
|
233
|
+
return false;
|
|
234
|
+
if (newTreeId !== entry.specTreeId)
|
|
235
|
+
return false;
|
|
236
|
+
// Tree-equivalent. Rebuild spec commit with the new head as the
|
|
237
|
+
// second parent so post-merge PR detection still works.
|
|
238
|
+
const branchSuffix = entry.branch.replace(/^.*\//, "").replace(/-/g, " ");
|
|
239
|
+
const message = `Merge PR #${entry.prNumber}: ${branchSuffix} (rebased)`;
|
|
240
|
+
let newSpecSha;
|
|
241
|
+
try {
|
|
242
|
+
newSpecSha = await git.commitTree(entry.specTreeId, [baseSha, newHead], message);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (!newSpecSha)
|
|
248
|
+
return false;
|
|
249
|
+
try {
|
|
250
|
+
await git.pushCommit(newSpecSha, entry.specBranch);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
emit(ctx, entry, "spec_build_succeeded", {
|
|
256
|
+
specBranch: entry.specBranch,
|
|
257
|
+
detail: "patch-id-equivalent rebuild",
|
|
258
|
+
});
|
|
259
|
+
// Plan §5.2: re-announce spec availability on the new head.
|
|
260
|
+
if (ctx.eviction.reportSpecReady) {
|
|
261
|
+
await ctx.eviction.reportSpecReady(entry, entry.specBranch, newSpecSha).catch(() => undefined);
|
|
262
|
+
}
|
|
263
|
+
let runId = null;
|
|
264
|
+
try {
|
|
265
|
+
runId = await ctx.ci.triggerRun(entry.specBranch, newSpecSha);
|
|
266
|
+
emit(ctx, entry, "ci_triggered", { ciRunId: runId, specBranch: entry.specBranch });
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
runId = null;
|
|
270
|
+
}
|
|
271
|
+
ctx.store.rebuildSpecHeadEquivalent(entry.id, {
|
|
272
|
+
headSha: newHead,
|
|
273
|
+
specSha: newSpecSha,
|
|
274
|
+
specBranch: entry.specBranch,
|
|
275
|
+
headPatchId: newPatchId,
|
|
276
|
+
specTreeId: newTreeId,
|
|
277
|
+
ciRunId: runId,
|
|
278
|
+
}, runId
|
|
279
|
+
? `patch-id-equivalent rebuild, CI ${runId.slice(0, 12)}`
|
|
280
|
+
: "patch-id-equivalent rebuild (CI trigger pending)");
|
|
281
|
+
return true;
|
|
282
|
+
}
|
package/dist/service-queue.js
CHANGED
|
@@ -63,6 +63,8 @@ export class MergeStewardQueueCommands {
|
|
|
63
63
|
postMergeSha: null,
|
|
64
64
|
postMergeSummary: null,
|
|
65
65
|
postMergeCheckedAt: null,
|
|
66
|
+
headPatchId: null,
|
|
67
|
+
specTreeId: null,
|
|
66
68
|
prTitle: params.prTitle ?? null,
|
|
67
69
|
enqueuedAt: new Date().toISOString(),
|
|
68
70
|
updatedAt: new Date().toISOString(),
|
package/dist/store.d.ts
CHANGED
|
@@ -11,9 +11,27 @@ export interface QueueStore {
|
|
|
11
11
|
listActive(repoId: string): QueueEntry[];
|
|
12
12
|
listAll(repoId: string): QueueEntry[];
|
|
13
13
|
insert(entry: QueueEntry): void;
|
|
14
|
-
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt">>, detail?: string): void;
|
|
14
|
+
transition(entryId: string, to: QueueEntryStatus, patch?: Partial<Pick<QueueEntry, "headSha" | "baseSha" | "ciRunId" | "ciRetries" | "retryAttempts" | "lastFailedBaseSha" | "specBranch" | "specSha" | "specBasedOn" | "waitDetail" | "postMergeStatus" | "postMergeSha" | "postMergeSummary" | "postMergeCheckedAt" | "headPatchId" | "specTreeId">>, detail?: string): void;
|
|
15
15
|
dequeue(entryId: string): void;
|
|
16
16
|
updateHead(entryId: string, newHeadSha: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Plan §5.3: patch-id-aware short-circuit. The new head is
|
|
19
|
+
* patch-id-equivalent to the cached identity AND the freshly
|
|
20
|
+
* computed merge tree matches the cached spec tree. We rebuild the
|
|
21
|
+
* spec commit with the new head as its second parent (so GitHub
|
|
22
|
+
* marks the PR merged after fast-forward) but do not blow away the
|
|
23
|
+
* spec branch or restart the prepare cycle. CI must re-run on the
|
|
24
|
+
* new spec SHA — check_runs are SHA-anchored and there is no API
|
|
25
|
+
* to reuse a passing verdict.
|
|
26
|
+
*/
|
|
27
|
+
rebuildSpecHeadEquivalent(entryId: string, patch: {
|
|
28
|
+
headSha: string;
|
|
29
|
+
specSha: string;
|
|
30
|
+
specBranch: string;
|
|
31
|
+
headPatchId: string;
|
|
32
|
+
specTreeId: string;
|
|
33
|
+
ciRunId: string | null;
|
|
34
|
+
}, detail?: string): void;
|
|
17
35
|
updatePriority(entryId: string, priority: number, detail?: string): void;
|
|
18
36
|
insertIncident(incident: IncidentRecord): void;
|
|
19
37
|
listIncidents(entryId: string): IncidentRecord[];
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,21 @@ export interface QueueEntry {
|
|
|
50
50
|
postMergeSummary: string | null;
|
|
51
51
|
/** Timestamp of the most recent post-merge verification attempt. */
|
|
52
52
|
postMergeCheckedAt: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Plan §5.3: stable patch-id (`git patch-id --stable`) of the
|
|
55
|
+
* current `headSha` against the entry's `baseSha`. Cached at spec
|
|
56
|
+
* build time so the prepare path can short-circuit on
|
|
57
|
+
* patch-id-equivalent updateHeads without re-running the merge.
|
|
58
|
+
* Null until the spec is first built.
|
|
59
|
+
*/
|
|
60
|
+
headPatchId: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Plan §5.3: tree id of `specSha` (i.e. `git rev-parse specSha^{tree}`).
|
|
63
|
+
* Cached at spec build time so we can compare against a freshly
|
|
64
|
+
* computed `merge-tree --write-tree` for the new head and decide
|
|
65
|
+
* whether the merged tree is unchanged.
|
|
66
|
+
*/
|
|
67
|
+
specTreeId: string | null;
|
|
53
68
|
enqueuedAt: string;
|
|
54
69
|
updatedAt: string;
|
|
55
70
|
}
|