g1t/apps/web/app/components/required-checks.tsx

137 lines5,741 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1/**
2 * Choosing the checks a branch requires: every name reported on the
3 * repository's commits lately, with the events it was reported for, ticked
4 * when it is required, and a field for a name nothing has reported yet.
5 * Posts each ticked name as `requiredChecks`.
6 */
7import { Plus, X } from "lucide-react";
8import { useState } from "react";
9
10import type { SeenCheck } from "@g1t/contracts";
11
12import { CheckboxOption } from "./ui/checkbox";
13import { TimeAgo } from "./ui";
14
15/** The events a workflow reports for that matter to a merge, first. */
16const EVENT_ORDER = ["pull_request", "merge_group", "pull_request_target", "push"];
17
18function events(list: string[]): string {
19 const sorted = [...list].sort((a, b) => {
20 const ia = EVENT_ORDER.indexOf(a);
21 const ib = EVENT_ORDER.indexOf(b);
22 return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);
23 });
24 return sorted.join(", ");
25}
26
27/** What to say under a name: where it was seen, and whether that is enough to require it. */
28function about(seen: SeenCheck | undefined, mergeQueue: boolean) {
29 if (!seen) return <>Not reported in the last 30 days. Until something reports it, nothing can merge.</>;
30 const onPulls = seen.events.length === 0 || seen.events.some((event) => event.startsWith("pull_request"));
31 const onQueue = seen.events.length === 0 || seen.events.includes("merge_group");
32 return (
33 <>
34 {seen.events.length > 0 ? `Reported for ${events(seen.events)}` : "Reported by a tool other than a workflow"} ·{" "}
35 <TimeAgo at={seen.lastSeen} />
36 {!onPulls && <span className="text-warn"> · not on pull requests, so it would hold every merge</span>}
37 {onPulls && mergeQueue && !onQueue && <span className="text-warn"> · not on merge_group, so the queue cannot pass</span>}
38 </>
39 );
40}
41
42export function RequiredChecksPicker({
43 required,
44 seen,
45 mergeQueue,
46 disabled,
47}: {
48 required: string[];
49 seen: SeenCheck[];
50 mergeQueue: boolean;
51 disabled?: boolean;
52}) {
53 const [added, setAdded] = useState<string[]>([]);
54 const [typed, setTyped] = useState("");
55 const known = (name: string, list: string[]) => list.some((other) => other.toLowerCase() === name.toLowerCase());
56 // Required ones first, as they are; then the rest seen lately; then what was typed.
57 const names = [
58 ...required,
59 ...seen.map((check) => check.name).filter((name) => !known(name, required)),
60 ...added.filter((name) => !known(name, required) && !seen.some((check) => check.name.toLowerCase() === name.toLowerCase())),
61 ];
62 const add = () => {
63 const name = typed.trim();
64 if (name && !known(name, names)) setAdded([...added, name]);
65 setTyped("");
66 };
67 return (
68 <div className="rounded-xl border border-line bg-surface p-4">
69 <p className="text-sm font-medium">Require status checks to pass before merging</p>
70 <p className="mt-1 text-sm text-muted">
71 A pull request merges only once each check ticked here has passed on its latest commit: for people and agents
72 alike, by hand, by auto-merge and through the queue. A workflow reports a check named after it.
73 </p>
74 {names.length === 0 ? (
75 <p className="mt-3 rounded-lg border border-dashed border-line px-3 py-3 text-sm text-faint">
76 No checks have been reported on this repository in the last 30 days.
77 </p>
78 ) : (
79 <ul className="mt-3 divide-y divide-line rounded-lg border border-line bg-bg">
80 {names.map((name) => {
81 const seenCheck = seen.find((check) => check.name.toLowerCase() === name.toLowerCase());
82 const isAdded = known(name, added) && !known(name, required);
83 return (
84 <li key={name} className="flex items-start gap-2 px-3 py-2.5">
85 <CheckboxOption
86 className="min-w-0 grow"
87 name="requiredChecks"
88 value={name}
89 defaultChecked={known(name, required) || isAdded}
90 disabled={disabled}
91 label={<span className="font-mono text-[0.8125rem]">{name}</span>}
92 description={about(seenCheck, mergeQueue)}
93 />
94 {isAdded && (
95 <button
96 type="button"
97 aria-label={`Remove ${name}`}
98 onClick={() => setAdded(added.filter((other) => other !== name))}
99 className="rounded-md p-1 text-faint hover:bg-raised hover:text-fg"
100 >
101 <X size={13} />
102 </button>
103 )}
104 </li>
105 );
106 })}
107 </ul>
108 )}
109 <div className="mt-3 flex flex-wrap gap-2">
110 <input
111 value={typed}
112 onChange={(event) => setTyped(event.target.value)}
113 onKeyDown={(event) => {
114 if (event.key === "Enter") {
115 event.preventDefault();
116 add();
117 }
118 }}
119 disabled={disabled}
120 maxLength={100}
121 placeholder="Another check, by name"
122 aria-label="Require another check, by name"
123 className="min-w-0 grow rounded-md border border-line bg-bg px-3 py-1.5 font-mono text-sm placeholder:font-sans placeholder:text-faint focus:border-line-strong focus:outline-none sm:max-w-xs"
124 />
125 <button
126 type="button"
127 onClick={add}
128 disabled={disabled || typed.trim() === ""}
129 className="inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm text-fg/80 hover:border-line-strong hover:bg-raised disabled:opacity-50"
130 >
131 <Plus size={13} />
132 Add
133 </button>
134 </div>
135 </div>
136 );
137}