| 1 | /** |
| 2 | * Words for a commit's checks, as the badge beside a commit and its list |
| 3 | * say them (components/commit-checks.tsx). |
| 4 | */ |
| 5 | import type { CheckItem, CommitChecks } from "@g1t/contracts"; |
| 6 | |
| 7 | /** The headline over a commit's checks. */ |
| 8 | export function checksHeadline(checks: Pick<CommitChecks, "state">): string { |
| 9 | if (checks.state === "failure") return "Some checks were not successful"; |
| 10 | if (checks.state === "pending") return "Some checks haven't completed yet"; |
| 11 | if (checks.state === "none") return "No checks"; |
| 12 | return "All checks have passed"; |
| 13 | } |
| 14 | |
| 15 | /** "1 failing, 1 in progress and 2 successful checks", under the headline. */ |
| 16 | export function checksTally(checks: Pick<CommitChecks, "successful" | "failed" | "pending" | "skipped" | "total">): string { |
| 17 | const parts: string[] = []; |
| 18 | if (checks.failed) parts.push(`${checks.failed} failing`); |
| 19 | if (checks.pending) parts.push(`${checks.pending} in progress`); |
| 20 | if (checks.skipped) parts.push(`${checks.skipped} skipped`); |
| 21 | if (checks.successful) parts.push(`${checks.successful} successful`); |
| 22 | const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : (parts[0] ?? "No"); |
| 23 | return `${list} ${checks.total === 1 ? "check" : "checks"}`; |
| 24 | } |
| 25 | |
| 26 | /** `25s`, `1m 12s` or `1h 4m` between two times; empty without both. */ |
| 27 | export function took(start: string | null, end: string | null): string { |
| 28 | if (!start || !end) return ""; |
| 29 | const seconds = Math.max(0, Math.round((new Date(end).getTime() - new Date(start).getTime()) / 1000)); |
| 30 | if (Number.isNaN(seconds)) return ""; |
| 31 | if (seconds < 60) return `${seconds}s`; |
| 32 | const minutes = Math.floor(seconds / 60); |
| 33 | if (minutes < 60) return `${minutes}m ${seconds % 60}s`; |
| 34 | return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * What one check says after its name: a status's description; for a check |
| 39 | * run, how it went and how long it took (`Successful in 25s`), then its |
| 40 | * title. |
| 41 | */ |
| 42 | export function checkDetail(item: CheckItem): string { |
| 43 | if (item.kind === "status") return item.description ?? stateWord(item); |
| 44 | const time = took(item.startedAt, item.completedAt); |
| 45 | let word = stateWord(item); |
| 46 | if (time && item.state === "success") word = `Successful in ${time}`; |
| 47 | else if (time && item.state === "failure") word = `Failing after ${time}`; |
| 48 | return item.description ? `${word} — ${item.description}` : word; |
| 49 | } |
| 50 | |
| 51 | function stateWord(item: Pick<CheckItem, "state" | "startedAt">): string { |
| 52 | switch (item.state) { |
| 53 | case "success": |
| 54 | return "Successful"; |
| 55 | case "failure": |
| 56 | return "Failing"; |
| 57 | case "pending": |
| 58 | return item.startedAt ? "In progress" : "Queued"; |
| 59 | case "cancelled": |
| 60 | return "Cancelled"; |
| 61 | case "skipped": |
| 62 | return "Skipped"; |
| 63 | default: |
| 64 | return "Neutral"; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Where a check's Details link goes: its page on g1t when it has one (a |
| 70 | * check run's report, or a job's run), otherwise the reporter's page. A |
| 71 | * link to g1t itself is made a path; anything else opens apart. |
| 72 | */ |
| 73 | export function detailsLink(item: Pick<CheckItem, "url" | "detailsUrl">, site = "https://g1t.sh"): { href: string; external: boolean } | null { |
| 74 | const target = item.url ?? item.detailsUrl; |
| 75 | if (!target) return null; |
| 76 | if (target.startsWith("/")) return { href: target, external: false }; |
| 77 | if (target.startsWith(`${site}/`)) return { href: target.slice(site.length), external: false }; |
| 78 | if (/^https?:\/\//.test(target)) return { href: target, external: true }; |
| 79 | return null; |
| 80 | } |