| 1 | import type { PullRequestCheck } from "@t3tools/contracts"; |
| 2 | |
| 3 | /** ISO-8601 timestamps in UTC compare correctly as plain text, which is all the ordering needs. */ |
| 4 | function 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 | */ |
| 30 | export 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 | } |