merge-steward 0.25.0 → 0.26.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.
- package/dist/db/schema.js +2 -0
- package/dist/db/sqlite-store.js +3 -2
- package/dist/github/pr-client.js +4 -1
- package/dist/reconciler-merge.d.ts +4 -0
- package/dist/reconciler-merge.js +36 -5
- package/dist/service-queue.d.ts +7 -0
- package/dist/service-queue.js +31 -0
- package/dist/types.d.ts +14 -1
- package/package.json +1 -1
package/dist/db/schema.js
CHANGED
|
@@ -76,6 +76,8 @@ 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 §8.4: stack-aware admission ordering needs the PR's base ref.
|
|
80
|
+
ensureColumn(connection, "queue_entries", "base_ref_name", "TEXT");
|
|
79
81
|
// Plan §5.3: cached identity for patch-id-aware updateHead.
|
|
80
82
|
ensureColumn(connection, "queue_entries", "head_patch_id", "TEXT");
|
|
81
83
|
ensureColumn(connection, "queue_entries", "spec_tree_id", "TEXT");
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -30,6 +30,7 @@ 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
|
+
baseRefName: row.base_ref_name === null || row.base_ref_name === undefined ? null : String(row.base_ref_name),
|
|
33
34
|
headPatchId: row.head_patch_id === null || row.head_patch_id === undefined ? null : String(row.head_patch_id),
|
|
34
35
|
specTreeId: row.spec_tree_id === null || row.spec_tree_id === undefined ? null : String(row.spec_tree_id),
|
|
35
36
|
enqueuedAt: String(row.enqueued_at),
|
|
@@ -128,9 +129,9 @@ export class SqliteStore {
|
|
|
128
129
|
priority, generation, ci_run_id, ci_retries, retry_attempts,
|
|
129
130
|
max_retries, last_failed_base_sha, issue_key, wait_detail,
|
|
130
131
|
post_merge_status, post_merge_sha, post_merge_summary, post_merge_checked_at,
|
|
131
|
-
pr_title,
|
|
132
|
+
pr_title, base_ref_name,
|
|
132
133
|
enqueued_at, updated_at)
|
|
133
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.waitDetail ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.prTitle ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
134
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.waitDetail ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.prTitle ?? null, entry.baseRefName ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
134
135
|
this.writeEvent(entry.id, null, entry.status);
|
|
135
136
|
})();
|
|
136
137
|
}
|
package/dist/github/pr-client.js
CHANGED
|
@@ -24,7 +24,9 @@ export class GitHubPRClient {
|
|
|
24
24
|
const result = await exec("gh", [
|
|
25
25
|
"pr", "view", String(prNumber),
|
|
26
26
|
"--repo", this.repoFullName,
|
|
27
|
-
|
|
27
|
+
// Plan §8.4 change 3a: include baseRefName so the queue can
|
|
28
|
+
// detect stacked PRs at admission time.
|
|
29
|
+
"--json", "number,title,headRefName,headRefOid,baseRefName,reviewDecision,state,mergeStateStatus",
|
|
28
30
|
], { githubRepoFullName: this.repoFullName });
|
|
29
31
|
const data = JSON.parse(result.stdout);
|
|
30
32
|
return {
|
|
@@ -32,6 +34,7 @@ export class GitHubPRClient {
|
|
|
32
34
|
branch: data.headRefName,
|
|
33
35
|
headSha: data.headRefOid,
|
|
34
36
|
...(data.title ? { title: data.title } : {}),
|
|
37
|
+
...(data.baseRefName ? { baseRefName: data.baseRefName } : {}),
|
|
35
38
|
mergeable: data.state === "OPEN",
|
|
36
39
|
mergeStateStatus: data.mergeStateStatus,
|
|
37
40
|
reviewDecision: data.reviewDecision,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { QueueEntry } from "./types.ts";
|
|
2
2
|
import type { ReconcileContext } from "./reconciler-core.ts";
|
|
3
3
|
export declare function mergeHead(ctx: ReconcileContext, entry: QueueEntry): Promise<void>;
|
|
4
|
+
export declare function deletePrBranchAfterGitHubMarksMerged(ctx: ReconcileContext, entry: QueueEntry, options?: {
|
|
5
|
+
attempts?: number;
|
|
6
|
+
delayMs?: number;
|
|
7
|
+
}): Promise<void>;
|
package/dist/reconciler-merge.js
CHANGED
|
@@ -2,6 +2,11 @@ import { CLEAN_CI, CLEAN_SPEC, emit, isBudgetExhausted, ref } from "./reconciler
|
|
|
2
2
|
import { cleanupSpec, evictEntry, invalidateDownstream } from "./reconciler-evict.js";
|
|
3
3
|
import { verifyPostMergeStatus } from "./reconciler-post-merge.js";
|
|
4
4
|
import { getEffectiveMainStatus } from "./reconciler-main-status.js";
|
|
5
|
+
const DEFAULT_PR_MERGED_POLL_ATTEMPTS = 6;
|
|
6
|
+
const DEFAULT_PR_MERGED_POLL_DELAY_MS = 2_000;
|
|
7
|
+
function delay(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
5
10
|
export async function mergeHead(ctx, entry) {
|
|
6
11
|
emit(ctx, entry, "merge_revalidating");
|
|
7
12
|
const prStatus = await ctx.github.getStatus(entry.prNumber);
|
|
@@ -133,10 +138,36 @@ export async function mergeHead(ctx, entry) {
|
|
|
133
138
|
postMergeCheckedAt: new Date().toISOString(),
|
|
134
139
|
}, `spec pushed to main; ${verificationResult.postMergeSummary}`);
|
|
135
140
|
await cleanupSpec(ctx, entry);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
await deletePrBranchAfterGitHubMarksMerged(ctx, entry);
|
|
142
|
+
}
|
|
143
|
+
export async function deletePrBranchAfterGitHubMarksMerged(ctx, entry, options = {}) {
|
|
144
|
+
const attempts = options.attempts ?? DEFAULT_PR_MERGED_POLL_ATTEMPTS;
|
|
145
|
+
const delayMs = options.delayMs ?? DEFAULT_PR_MERGED_POLL_DELAY_MS;
|
|
146
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
147
|
+
let merged = false;
|
|
148
|
+
try {
|
|
149
|
+
merged = (await ctx.github.getStatus(entry.prNumber)).merged;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Keep polling briefly. If GitHub is unavailable, branch cleanup is
|
|
153
|
+
// cosmetic; preserving correct PR merge classification matters more.
|
|
154
|
+
}
|
|
155
|
+
if (merged) {
|
|
156
|
+
try {
|
|
157
|
+
await ctx.github.deleteBranch(entry.prNumber);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
emit(ctx, entry, "pr_branch_cleanup_failed", {
|
|
161
|
+
detail: "GitHub marked the PR merged, but deleting the head branch failed",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (attempt < attempts - 1 && delayMs > 0) {
|
|
167
|
+
await delay(delayMs);
|
|
168
|
+
}
|
|
141
169
|
}
|
|
170
|
+
emit(ctx, entry, "pr_branch_cleanup_deferred", {
|
|
171
|
+
detail: "waiting for GitHub to classify the fast-forwarded PR as merged before deleting the head branch",
|
|
172
|
+
});
|
|
142
173
|
}
|
package/dist/service-queue.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export declare class MergeStewardQueueCommands {
|
|
|
19
19
|
issueKey?: string;
|
|
20
20
|
priority?: number;
|
|
21
21
|
prTitle?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Plan §8.4: PR's base ref. When this matches another open PR's
|
|
24
|
+
* `branch` (head ref), the entry is stacked and admission orders
|
|
25
|
+
* it immediately behind the parent.
|
|
26
|
+
*/
|
|
27
|
+
baseRefName?: string;
|
|
22
28
|
}): QueueEntry | undefined;
|
|
23
29
|
scanStartupAdmissions(): Promise<void>;
|
|
24
30
|
scanEligibleOpenPrs(): Promise<{
|
|
@@ -34,4 +40,5 @@ export declare class MergeStewardQueueCommands {
|
|
|
34
40
|
private findAffectedEntriesAfterPriorityChange;
|
|
35
41
|
private requeueAffectedEntries;
|
|
36
42
|
private nextPosition;
|
|
43
|
+
private findActiveEntryByBranch;
|
|
37
44
|
}
|
package/dist/service-queue.js
CHANGED
|
@@ -66,6 +66,7 @@ export class MergeStewardQueueCommands {
|
|
|
66
66
|
headPatchId: null,
|
|
67
67
|
specTreeId: null,
|
|
68
68
|
prTitle: params.prTitle ?? null,
|
|
69
|
+
baseRefName: params.baseRefName ?? null,
|
|
69
70
|
enqueuedAt: new Date().toISOString(),
|
|
70
71
|
updatedAt: new Date().toISOString(),
|
|
71
72
|
};
|
|
@@ -185,12 +186,36 @@ export class MergeStewardQueueCommands {
|
|
|
185
186
|
return false;
|
|
186
187
|
}
|
|
187
188
|
}
|
|
189
|
+
// Plan §8.4: stack-aware admission. When the PR's base ref
|
|
190
|
+
// names another open PR's branch (head ref), the entry is
|
|
191
|
+
// stacked. Enqueue it only when the parent is already in the
|
|
192
|
+
// queue — otherwise the child's spec would build against the
|
|
193
|
+
// unmerged parent's pre-spec content. Positions are monotonic
|
|
194
|
+
// (`nextPosition`), so the child's position is always greater
|
|
195
|
+
// than the parent's; sibling PRs admitted between the parent's
|
|
196
|
+
// enqueue and the child's tryAdmit can sit between them. That's
|
|
197
|
+
// fine: queue head selection orders by (priority, position), so
|
|
198
|
+
// the parent is still processed before the child. The functional
|
|
199
|
+
// guarantee is "parent before child", not strict adjacency.
|
|
200
|
+
const baseRefName = status.baseRefName ?? null;
|
|
201
|
+
if (baseRefName && baseRefName !== this.config.baseBranch) {
|
|
202
|
+
const parentEntry = this.findActiveEntryByBranch(baseRefName);
|
|
203
|
+
if (!parentEntry) {
|
|
204
|
+
this.logger.debug({
|
|
205
|
+
prNumber,
|
|
206
|
+
baseRefName,
|
|
207
|
+
repoBaseBranch: this.config.baseBranch,
|
|
208
|
+
}, "Deferring admission for stacked PR — parent branch is not in the queue yet");
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
188
212
|
this.enqueue({
|
|
189
213
|
prNumber,
|
|
190
214
|
branch,
|
|
191
215
|
headSha,
|
|
192
216
|
priority,
|
|
193
217
|
...(status.title ? { prTitle: status.title } : {}),
|
|
218
|
+
...(baseRefName ? { baseRefName } : {}),
|
|
194
219
|
});
|
|
195
220
|
return true;
|
|
196
221
|
}
|
|
@@ -279,4 +304,10 @@ export class MergeStewardQueueCommands {
|
|
|
279
304
|
}
|
|
280
305
|
return next;
|
|
281
306
|
}
|
|
307
|
+
// Plan §8.4: lookup helper used by stack-aware admission. Returns
|
|
308
|
+
// the active queue entry whose `branch` (head ref) matches `name`,
|
|
309
|
+
// or undefined when no such entry is in the queue.
|
|
310
|
+
findActiveEntryByBranch(name) {
|
|
311
|
+
return this.store.listActive(this.config.repoId).find((entry) => entry.branch === name);
|
|
312
|
+
}
|
|
282
313
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ 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 §8.4: PR's base ref captured at admission. When this names
|
|
55
|
+
* another open PR's `branch` (head ref), the entry is stacked and
|
|
56
|
+
* the queue holds it behind the parent until the parent is itself
|
|
57
|
+
* admitted. `null` when the PR was opened against the repo default.
|
|
58
|
+
*/
|
|
59
|
+
baseRefName: string | null;
|
|
53
60
|
/**
|
|
54
61
|
* Plan §5.3: stable patch-id (`git patch-id --stable`) of the
|
|
55
62
|
* current `headSha` against the entry's `baseSha`. Cached at spec
|
|
@@ -198,6 +205,12 @@ export interface PRStatus {
|
|
|
198
205
|
branch: string;
|
|
199
206
|
headSha: string;
|
|
200
207
|
title?: string | undefined;
|
|
208
|
+
/**
|
|
209
|
+
* Plan §8.4: PR's base ref. When this names another open PR's
|
|
210
|
+
* `branch` (head ref), the PR is stacked and admission must defer
|
|
211
|
+
* until the parent is in the queue.
|
|
212
|
+
*/
|
|
213
|
+
baseRefName?: string | undefined;
|
|
201
214
|
mergeable: boolean;
|
|
202
215
|
mergeStateStatus?: string | undefined;
|
|
203
216
|
reviewDecision?: string | undefined;
|
|
@@ -211,7 +224,7 @@ export interface QueueConfig {
|
|
|
211
224
|
flakyRetries: number;
|
|
212
225
|
pollIntervalMs: number;
|
|
213
226
|
}
|
|
214
|
-
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "spec_build_started" | "spec_build_succeeded" | "spec_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "merge_waiting_main" | "main_pending_bypassed" | "sanitized_closed" | "sanitized_duplicate" | "post_merge_verification_started" | "post_merge_verification_completed" | "branch_unreachable";
|
|
227
|
+
export type ReconcileAction = "promoted" | "fetch_started" | "main_broken" | "branch_mismatch" | "spec_build_started" | "spec_build_succeeded" | "spec_build_conflict" | "ci_triggered" | "ci_pending" | "ci_passed" | "ci_failed" | "ci_flaky_retry" | "merge_revalidating" | "merge_succeeded" | "merge_rejected" | "merge_external" | "policy_changed" | "evicted" | "invalidated" | "retry_gated" | "budget_exhausted" | "merge_waiting_approval" | "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" | "branch_unreachable";
|
|
215
228
|
export interface ReconcileEvent {
|
|
216
229
|
at: string;
|
|
217
230
|
entryId: string;
|