Skip to content

Commit 6744f95

Browse files
committed
Merge main b4a1977 (F6b #161 squash) into the #48 browser fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/implementation/merge-handoff.md # github/merge.ts # runner/merge-job.ts # runner/merge.ts # runner/store.ts # test/browser/review.spec.ts # test/merge-job.test.ts # test/merge-task-pr.test.ts # test/merge.test.ts # test/runner-production.test.ts # test/runner-publishing.test.ts # web/public/app.js # web/server.ts
2 parents 792581a + b4a1977 commit 6744f95

13 files changed

Lines changed: 1955 additions & 282 deletions

File tree

‎docs/implementation/merge-handoff.md‎

Lines changed: 16 additions & 11 deletions
Large diffs are not rendered by default.

‎github/merge.ts‎

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ function timestamp(value: unknown, label: string): string {
149149
return value;
150150
}
151151

152-
/** GitHub CLI adapter. All arguments are literal argv; no shell is involved. */
153152
/**
154153
* The state GitHub gives one observed required check. A check run that completed SUCCESS, NEUTRAL or SKIPPED passes, as
155154
* GitHub treats those conclusions for required checks; a commit status passes only with SUCCESS.
@@ -165,18 +164,34 @@ function observedCheckState(check: Record<string, unknown>): 'success' | 'pendin
165164
const status = String(check.state ?? '').toUpperCase();
166165
return status === 'SUCCESS' ? 'success' : ['PENDING','EXPECTED'].includes(status) ? 'pending' : 'failure';
167166
}
167+
/** A real time gh reports, or NaN for none or the zero time (year 1) it gives a run that has not started or finished. */
168+
function observedTime(value: unknown): number {
169+
const at = typeof value === 'string' ? Date.parse(value) : NaN;
170+
return Number.isFinite(at) && new Date(at).getUTCFullYear() > 1 ? at : NaN;
171+
}
168172
/**
169-
* When an observed check run or status started, to pick the latest of several with one name. gh reports a run that has
170-
* not started yet with no time or the zero time (year 1); such a run was queued after every started one, so it counts
171-
* as the latest.
173+
* When an observed check run or status started, to pick the latest of several with one name, or NaN when that is not
174+
* known. A run or commit status not yet completed (queued, in progress, pending) with no start time was queued after
175+
* every started one, so it counts as the latest (+Infinity). A completed one with no start time gets NaN: its order
176+
* relative to the latest is unknown (see `observedCheckOlderThan`).
172177
*/
173178
function observedCheckStart(check: Record<string, unknown>): number {
174-
const at = typeof check.startedAt === 'string' ? Date.parse(check.startedAt) : NaN;
175-
return Number.isFinite(at) && new Date(at).getUTCFullYear() > 1 ? at : Number.POSITIVE_INFINITY;
179+
const started = observedTime(check.startedAt);
180+
if (Number.isFinite(started)) return started;
181+
return observedCheckState(check) === 'pending' ? Number.POSITIVE_INFINITY : NaN;
182+
}
183+
/**
184+
* For a completed run or commit status with no start time: its completion time, before which it started, so it is
185+
* known to be older than any observation of its kind that started after that time. NaN when it has a start time, is not
186+
* completed, or has no completion time.
187+
*/
188+
function observedCheckOlderThan(check: Record<string, unknown>): number {
189+
return Number.isNaN(observedCheckStart(check)) ? observedTime(check.completedAt) : NaN;
176190
}
177191
/** Among runs with the same start, the one that passes least counts, so a tie never reads as passing. */
178192
const checkStateRank = { failure: 0, pending: 1, success: 2 } as const;
179193

194+
/** GitHub CLI adapter. All arguments are literal argv; no shell is involved. */
180195
export class GhMergeGateway implements MergeGateway, MergeQueueGateway {
181196
readonly config: GhMergeConfig;
182197
readonly run: RunGh;
@@ -400,10 +415,23 @@ export class GhMergeGateway implements MergeGateway, MergeQueueGateway {
400415
});
401416
if (!matches.length) return { ...required, state: 'missing' as const };
402417
// A re-run adds a newer run (or status) with the same name; GitHub counts the latest one, so an older failure
403-
// does not outrank a newer pending or passing run.
404-
const latest = matches.map(check => ({ state: observedCheckState(check), at: observedCheckStart(check) }))
405-
.reduce((best, next) => next.at > best.at || (next.at === best.at && checkStateRank[next.state] < checkStateRank[best.state]) ? next : best);
406-
return { ...required, state: latest.state };
418+
// does not outrank a newer pending or passing run. A check run and a commit status with the same name are not
419+
// re-runs of each other: GitHub requires both to pass. So the latest of each kind counts, and the one that passes
420+
// least decides.
421+
// The latest is the observation with the greatest known start (a not-completed one with no start counts as
422+
// +Infinity). A completed observation with no start time is dropped when another of its kind started (or is
423+
// not-completed with no start) strictly after its completion: it is known to be older. Otherwise its order is
424+
// unknown, so it ties with the latest, and it can only make the result pass less.
425+
const latestOfKind = (kind: Record<string, unknown>[]) => {
426+
if (kind.length === 0) return null;
427+
const seen = kind.map(check => ({ state: observedCheckState(check), at: observedCheckStart(check), olderThan: observedCheckOlderThan(check) }));
428+
const latest = Math.max(...seen.map(check => Number.isNaN(check.at) ? Number.NEGATIVE_INFINITY : check.at));
429+
return seen.filter(check => Number.isNaN(check.at) ? !(latest > check.olderThan) : check.at === latest)
430+
.reduce((worst, next) => checkStateRank[next.state] < checkStateRank[worst.state] ? next : worst).state;
431+
};
432+
const states = [latestOfKind(matches.filter(check => typeof check.name === 'string')), latestOfKind(matches.filter(check => typeof check.name !== 'string'))]
433+
.filter(state => state !== null);
434+
return { ...required, state: states.reduce((worst, next) => checkStateRank[next] < checkStateRank[worst] ? next : worst) };
407435
});
408436
return { rulesKnown, atomicBaseGuard, mergeQueue, requiredChecks };
409437
}

0 commit comments

Comments
 (0)