merge-steward 0.5.4 → 0.5.6

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.
@@ -24,7 +24,7 @@ export class GitHubPRClient {
24
24
  const result = await exec("gh", [
25
25
  "pr", "view", String(prNumber),
26
26
  "--repo", this.repoFullName,
27
- "--json", "number,headRefName,headRefOid,reviewDecision,state",
27
+ "--json", "number,headRefName,headRefOid,reviewDecision,state,mergeStateStatus",
28
28
  ], { githubRepoFullName: this.repoFullName });
29
29
  const data = JSON.parse(result.stdout);
30
30
  return {
@@ -32,6 +32,7 @@ export class GitHubPRClient {
32
32
  branch: data.headRefName,
33
33
  headSha: data.headRefOid,
34
34
  mergeable: data.state === "OPEN",
35
+ mergeStateStatus: data.mergeStateStatus,
35
36
  reviewApproved: data.reviewDecision === "APPROVED",
36
37
  merged: data.state === "MERGED",
37
38
  };
@@ -114,6 +114,19 @@ async function prepareHead(ctx, entry) {
114
114
  // Gate: non-spinning — skip if base hasn't changed since last conflict.
115
115
  if (isRetryGated(entry, baseSha)) {
116
116
  emit(ctx, entry, "retry_gated", { baseSha, detail: "base unchanged since last conflict" });
117
+ try {
118
+ const prStatus = await ctx.github.getStatus(entry.prNumber);
119
+ if (prStatus.mergeStateStatus === "DIRTY") {
120
+ emit(ctx, entry, "budget_exhausted", {
121
+ baseSha,
122
+ detail: "retry gated and GitHub still reports merge conflict",
123
+ });
124
+ await evictEntry(ctx, entry, "integration_conflict");
125
+ }
126
+ }
127
+ catch {
128
+ // Best-effort check. If GitHub status is unavailable, preserve the non-spinning wait.
129
+ }
117
130
  return;
118
131
  }
119
132
  await performBranchRefresh(ctx, entry, baseSha);
@@ -136,7 +149,7 @@ function summarizeCheckNames(checks, limit = 3) {
136
149
  return `${names.slice(0, limit).join(", ")} +${names.length - limit} more`;
137
150
  }
138
151
  async function performBranchRefresh(ctx, entry, baseSha) {
139
- emit(ctx, entry, "rebase_started", { baseSha });
152
+ emit(ctx, entry, "rebase_started", { baseSha, detail: `refreshing ${entry.branch} with ${ctx.baseBranch}` });
140
153
  const result = await ctx.git.mergeBaseInto(entry.branch, ref(ctx, ctx.baseBranch));
141
154
  if (!result.success) {
142
155
  emit(ctx, entry, "rebase_conflict", { baseSha, conflictFiles: result.conflictFiles });
@@ -164,7 +177,7 @@ async function performBranchRefresh(ctx, entry, baseSha) {
164
177
  return;
165
178
  }
166
179
  await ctx.git.push(entry.branch, false);
167
- emit(ctx, entry, "rebase_succeeded", { baseSha });
180
+ emit(ctx, entry, "rebase_succeeded", { baseSha, detail: `refreshed ${entry.branch} with ${ctx.baseBranch}` });
168
181
  // Build speculative branch for downstream entries.
169
182
  let specBranch = null;
170
183
  let specSha = null;
@@ -186,7 +199,7 @@ async function performBranchRefresh(ctx, entry, baseSha) {
186
199
  ctx.store.transition(entry.id, "validating", {
187
200
  headSha, baseSha, ciRunId: runId, lastFailedBaseSha: null,
188
201
  specBranch, specSha, specBasedOn: null,
189
- }, `rebase onto ${baseSha.slice(0, 8)}, CI ${runId}`);
202
+ }, `refreshed with ${ctx.baseBranch} @ ${baseSha.slice(0, 8)}, CI ${runId}`);
190
203
  }
191
204
  // ─── Non-head entry: speculative branch ─────────────────────────
192
205
  async function prepareSpeculative(ctx, entry, prevEntry) {
package/dist/service.js CHANGED
@@ -222,6 +222,10 @@ export class MergeStewardService {
222
222
  updateHeadByPR(prNumber, headSha) {
223
223
  const entry = this.store.getEntryByPR(this.config.repoId, prNumber);
224
224
  if (entry) {
225
+ if (entry.headSha === headSha) {
226
+ this.logger.debug({ prNumber, entryId: entry.id, headSha }, "Ignoring synchronize webhook for unchanged head");
227
+ return;
228
+ }
225
229
  this.store.updateHead(entry.id, headSha);
226
230
  this.logger.info({ prNumber, entryId: entry.id, headSha }, "PR head updated via webhook");
227
231
  }
package/dist/types.d.ts CHANGED
@@ -158,6 +158,7 @@ export interface PRStatus {
158
158
  branch: string;
159
159
  headSha: string;
160
160
  mergeable: boolean;
161
+ mergeStateStatus?: string | undefined;
161
162
  reviewApproved: boolean;
162
163
  merged: boolean;
163
164
  }
@@ -76,7 +76,7 @@ export function nextStepLabel(status) {
76
76
  case "queued":
77
77
  return "waiting for head-of-line turn";
78
78
  case "preparing_head":
79
- return "rebasing onto latest main";
79
+ return "refreshing branch with latest main";
80
80
  case "validating":
81
81
  return "waiting for CI result";
82
82
  case "merging":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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": {