1import type { PullRequestCheck } from "@t3tools/contracts";
2
3/** ISO-8601 timestamps in UTC compare correctly as plain text, which is all the ordering needs. */
4function isAtLeastAsNew(candidate: string | null, kept: string | null): boolean {
5 if (candidate === null) return kept === null;
6 return kept === null || candidate >= kept;
7}
8
9/**
10 * One row per check rather than one per run of it.
11 *
12 * A host's rollup is a list of runs, not a list of checks: while a workflow is being re-run — or
13 * while a second run of it is already live — the same check arrives twice, and both copies reach
14 * the reader as what looks like a duplicate. Nothing in a check carries an id, so the name is what
15 * identifies it, qualified by the workflow it belongs to since two workflows are free to name a
16 * job the same thing.
17 *
18 * Within a group the newest run is the one worth showing: a re-run is the answer that replaces the
19 * one before it, and a run that never said when it happened loses to one that did. A tie goes to
20 * whichever came last, because a host lists a re-run after the run it repeats.
21 *
22 * The order is the host's own, held at the place each check first appeared, so a re-run landing
23 * mid-read replaces a row where it stands instead of reshuffling the list under the reader.
24 *
25 * Two checks that survive under the same name are then genuinely different ones, since they came
26 * from different workflows — each is shown as `workflow / name`, the way GitHub writes it itself.
27 * A survivor whose workflow the host did not name keeps its bare name rather than being qualified
28 * with nothing.
29 */
30export function dedupeChecks(
31 entries: ReadonlyArray<{
32 readonly check: PullRequestCheck;
33 readonly workflowName: string | null;
34 readonly at: string | null;
35 }>,
36): ReadonlyArray<PullRequestCheck> {
37 const newestByCheck = new Map<string, (typeof entries)[number]>();
38 for (const entry of entries) {
39 const key = JSON.stringify([entry.workflowName ?? "", entry.check.name]);
40 const kept = newestByCheck.get(key);
41 // Re-setting a key a Map already holds keeps its first position, which is the order wanted.
42 if (kept === undefined || isAtLeastAsNew(entry.at, kept.at)) newestByCheck.set(key, entry);
43 }
44 const survivors = [...newestByCheck.values()];
45 const countsByName = new Map<string, number>();
46 for (const entry of survivors) {
47 countsByName.set(entry.check.name, (countsByName.get(entry.check.name) ?? 0) + 1);
48 }
49 return survivors.map((entry) => {
50 const workflowName = entry.workflowName ?? "";
51 return workflowName.length > 0 && (countsByName.get(entry.check.name) ?? 0) > 1
52 ? { ...entry.check, name: `${workflowName} / ${entry.check.name}` }
53 : entry.check;
54 });
55}