| 1 | import type { MergebotPullRequestLiveStatus } from "@t3tools/contracts"; |
| 2 | |
| 3 | export type MergebotStatusPillTone = "yellow" | "red" | "orange" | "blue" | "green" | "muted"; |
| 4 | |
| 5 | export interface MergebotStatusPill { |
| 6 | readonly kind: |
| 7 | | "running-ci" |
| 8 | | "rebasing" |
| 9 | | "ci-failed" |
| 10 | | "merge-conflicts" |
| 11 | | "draft-passes-ci" |
| 12 | | "ready" |
| 13 | | "queued" |
| 14 | | "merging" |
| 15 | | "merged" |
| 16 | | "deploying" |
| 17 | | "deploy-failed" |
| 18 | | "on-staging" |
| 19 | | "shipped"; |
| 20 | readonly label: string; |
| 21 | readonly tone: MergebotStatusPillTone; |
| 22 | readonly etaAt: string | null; |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * One pill for a PR's place in the Mergebot lifecycle, or null when nothing is worth showing. |
| 27 | * Blockers outrank progress: conflicts and failures show even while the queue holds the PR. |
| 28 | */ |
| 29 | export function mergebotStatusPill( |
| 30 | status: MergebotPullRequestLiveStatus, |
| 31 | ): MergebotStatusPill | null { |
| 32 | if (status.lifecycle === "closed") return null; |
| 33 | |
| 34 | if (status.lifecycle === "merged") { |
| 35 | switch (status.deploy?.state) { |
| 36 | case "deploying": |
| 37 | return { kind: "deploying", label: "Deploying", tone: "green", etaAt: status.deploy.etaAt }; |
| 38 | case "failed": |
| 39 | return { kind: "deploy-failed", label: "Deploy Failed", tone: "red", etaAt: null }; |
| 40 | case "staging": |
| 41 | return { kind: "on-staging", label: "On Staging", tone: "green", etaAt: null }; |
| 42 | case "production": |
| 43 | return { kind: "shipped", label: "Shipped to Prod", tone: "muted", etaAt: null }; |
| 44 | default: |
| 45 | return { kind: "merged", label: "Merged", tone: "green", etaAt: null }; |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | const merging: MergebotStatusPill = { |
| 50 | kind: "merging", |
| 51 | label: "Merging", |
| 52 | tone: "green", |
| 53 | etaAt: status.mergeEtaAt, |
| 54 | }; |
| 55 | if (status.queueStatus === "merging") return merging; |
| 56 | if (status.hasConflicts === true) |
| 57 | return { kind: "merge-conflicts", label: "Merge Conflicts", tone: "orange", etaAt: null }; |
| 58 | if (status.ciStatus === "failed" || status.queueStatus === "ci_failed") |
| 59 | return { kind: "ci-failed", label: "CI Failed", tone: "red", etaAt: null }; |
| 60 | if (status.queueStatus === "updating_branch") |
| 61 | return { kind: "rebasing", label: "Rebasing", tone: "yellow", etaAt: status.mergeEtaAt }; |
| 62 | |
| 63 | if (status.ciStatus === "pending" || status.queueStatus === "waiting_ci") { |
| 64 | const counts = status.checks?.total |
| 65 | ? ` (${status.checks.completed}/${status.checks.total})` |
| 66 | : ""; |
| 67 | return { |
| 68 | kind: "running-ci", |
| 69 | label: `Running CI${counts}`, |
| 70 | tone: "yellow", |
| 71 | etaAt: status.mergeEtaAt, |
| 72 | }; |
| 73 | } |
| 74 | |
| 75 | if (status.position !== null && status.position >= 2) |
| 76 | return { |
| 77 | kind: "queued", |
| 78 | label: `Queued #${status.position}`, |
| 79 | tone: "blue", |
| 80 | etaAt: status.mergeEtaAt, |
| 81 | }; |
| 82 | if (status.ciStatus !== "passed") return null; |
| 83 | // The head of the queue with passing CI is merging, whatever its queue status says. |
| 84 | if (status.position === 1) return merging; |
| 85 | if (status.draft) |
| 86 | return { kind: "draft-passes-ci", label: "Draft Passes CI", tone: "blue", etaAt: null }; |
| 87 | return { kind: "ready", label: "Ready to merge", tone: "blue", etaAt: null }; |
| 88 | } |