1import type { MergebotPullRequestLiveStatus } from "@t3tools/contracts";
2
3export type MergebotStatusPillTone = "yellow" | "red" | "orange" | "blue" | "green" | "muted";
4
5export 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 */
29export 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}