merge-steward 0.25.0 → 0.26.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 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");
@@ -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
  }
@@ -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
- "--json", "number,title,headRefName,headRefOid,reviewDecision,state,mergeStateStatus",
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,
@@ -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
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Serial merge queue for GitHub — rebase, CI-gate, and merge PRs one at a time",
5
5
  "type": "module",
6
6
  "repository": {