merge-steward 0.10.1 → 0.10.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.
@@ -3,6 +3,39 @@ import { formatDuration, humanStatus, nextStepLabel, relativeTime, summarizeQueu
3
3
  const RECENT_EVICTION_MS = 60 * 60 * 1_000;
4
4
  const STUCK_ENTRY_MS = 25 * 60 * 1_000;
5
5
  const OFFLINE_STALE_MS = 10 * 1_000;
6
+ function isLaterEntry(left, right) {
7
+ if (left.position !== right.position) {
8
+ return left.position > right.position;
9
+ }
10
+ return new Date(left.updatedAt).getTime() >= new Date(right.updatedAt).getTime();
11
+ }
12
+ function getLatestEntries(snapshot) {
13
+ if (!snapshot) {
14
+ return [];
15
+ }
16
+ const byPR = new Map();
17
+ for (const entry of snapshot.entries) {
18
+ const existing = byPR.get(entry.prNumber);
19
+ if (!existing || isLaterEntry(entry, existing)) {
20
+ byPR.set(entry.prNumber, entry);
21
+ }
22
+ }
23
+ return [...byPR.values()].sort((a, b) => a.position - b.position);
24
+ }
25
+ function getMostRecentActivityAt(snapshot) {
26
+ const latestEntries = getLatestEntries(snapshot);
27
+ if (latestEntries.length === 0) {
28
+ return undefined;
29
+ }
30
+ return latestEntries.reduce((latest, entry) => {
31
+ if (!latest) {
32
+ return entry.updatedAt;
33
+ }
34
+ return new Date(entry.updatedAt).getTime() > new Date(latest).getTime()
35
+ ? entry.updatedAt
36
+ : latest;
37
+ }, undefined);
38
+ }
6
39
  export function matchRepoRef(repo, repoRef) {
7
40
  if (!repoRef) {
8
41
  return false;
@@ -22,10 +55,7 @@ export function getChainEntries(snapshot) {
22
55
  return buildChainEntries(snapshot.entries);
23
56
  }
24
57
  export function getActiveEntries(snapshot) {
25
- if (!snapshot) {
26
- return [];
27
- }
28
- return snapshot.entries.filter((entry) => (entry.status !== "merged" && entry.status !== "evicted" && entry.status !== "dequeued"));
58
+ return getLatestEntries(snapshot).filter((entry) => (entry.status !== "merged" && entry.status !== "evicted" && entry.status !== "dequeued"));
29
59
  }
30
60
  export function getDefaultEntryId(snapshot, filter, preferredPrNumber) {
31
61
  if (!snapshot) {
@@ -81,7 +111,7 @@ export function getRepoHealth(repo, now = Date.now()) {
81
111
  };
82
112
  }
83
113
  }
84
- const recentEvicted = snapshot.entries.find((entry) => (entry.status === "evicted"
114
+ const recentEvicted = getLatestEntries(snapshot).find((entry) => (entry.status === "evicted"
85
115
  && now - new Date(entry.updatedAt).getTime() <= RECENT_EVICTION_MS));
86
116
  if (recentEvicted) {
87
117
  return {
@@ -106,7 +136,7 @@ export function getRepoHealth(repo, now = Date.now()) {
106
136
  label: "Idle",
107
137
  color: "green",
108
138
  detail: snapshot.summary.total > 0
109
- ? `No active queue work right now. Last activity was ${relativeTime(snapshot.entries[0]?.updatedAt)} ago.`
139
+ ? `No active queue work right now. Last activity was ${relativeTime(getMostRecentActivityAt(snapshot))} ago.`
110
140
  : "No pull requests are queued right now.",
111
141
  };
112
142
  }
@@ -143,19 +173,23 @@ export function projectStatsSummary(snapshot) {
143
173
  if (!snapshot) {
144
174
  return "No queue data yet.";
145
175
  }
146
- const { summary } = snapshot;
176
+ const latestEntries = getLatestEntries(snapshot);
147
177
  const activeEntries = getActiveEntries(snapshot);
148
178
  const avgWaitMs = activeEntries.length > 0
149
179
  ? activeEntries.reduce((sum, entry) => sum + (Date.now() - new Date(entry.enqueuedAt).getTime()), 0) / activeEntries.length
150
180
  : 0;
181
+ const queued = latestEntries.filter((entry) => entry.status === "queued").length;
182
+ const validating = latestEntries.filter((entry) => entry.status === "validating" || entry.status === "preparing_head").length;
183
+ const merging = latestEntries.filter((entry) => entry.status === "merging").length;
184
+ const evicted = latestEntries.filter((entry) => entry.status === "evicted").length;
151
185
  const parts = [
152
- `${summary.active} active`,
153
- `${summary.queued} waiting`,
154
- `${summary.validating} testing`,
155
- `${summary.merging} merging`,
186
+ `${activeEntries.length} active`,
187
+ `${queued} waiting`,
188
+ `${validating} testing`,
189
+ `${merging} merging`,
156
190
  ];
157
- if (summary.evicted > 0) {
158
- parts.push(`${summary.evicted} need repair`);
191
+ if (evicted > 0) {
192
+ parts.push(`${evicted} need repair`);
159
193
  }
160
194
  if (avgWaitMs > 0) {
161
195
  parts.push(`avg wait ${formatDuration(avgWaitMs)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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": {