Skip to content

g1t/apps/web/app/components/dependency-updates.tsx

421 lines15,638 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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1/**
2 * The dependency update file, as a project's Security page shows it: which
3 * file was read, its problems with their lines, each `updates` entry with
4 * its schedule and last check, the private registries, the update pull
5 * requests g1t has open, and ignore conditions set in comments. "Check for
6 * updates" posts the `check_updates` intent to its page, the Security
7 * section's Dependency updates tab (`routes/repo/security-updates.tsx`).
8 */
9import {
10 CalendarClock,
11 CircleCheck,
12 CircleDot,
13 ExternalLink,
14 FileCode,
15 FileWarning,
16 GitMerge,
17 GitPullRequest,
18 Info,
19 KeyRound,
20 Loader,
21 RefreshCw,
22 Wrench,
23 EyeOff,
24} from "lucide-react";
25import { Link, useFetcher } from "react-router";
26
27import type { UpdatePull, VersionUpdateEntry, VersionUpdatesState } from "@g1t/contracts";
28
29import {
30 DEPENDENCY_UPDATES_DOCS,
31 PULL_STATES,
32 conditionText,
33 entryStatus,
34 groupText,
35 ignoreText,
36 livePulls,
37 timeUntil,
38 utc,
39} from "../lib/dependency-updates";
40import { TimeAgo } from "./ui";
41import { Badge, type BadgeTone } from "./ui/badge";
Merge branch 'worktree-agent-ae1299e92e4462012'42import { Hint } from "./ui/hint";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar43
44type Done = { ok: boolean; error?: string } | undefined;
45
46const PULL_TONES: Record<UpdatePull["state"], BadgeTone> = {
47 requested: "info",
48 open: "accent",
49 merged: "merged",
50 closed: "neutral",
51 superseded: "neutral",
52 needs_code: "warn",
53 failed: "danger",
54};
55
56/** Text with `code` spans, as the service writes its messages. */
57function Said({ text }: { text: string }) {
58 return (
59 <>
60 {text.split("`").map((part, index) =>
61 index % 2 === 1 ? (
62 <code key={index} className="text-fg-soft">
63 {part}
64 </code>
65 ) : (
66 part
67 ),
68 )}
69 </>
70 );
71}
72
73function Heading({ children }: { children: React.ReactNode }) {
74 return <h4 className="text-xs font-medium tracking-wide text-faint uppercase">{children}</h4>;
75}
76
77/** One `updates` entry: what it covers, when it runs, what it found. */
78function Entry({ entry, action, canCheck }: { entry: VersionUpdateEntry; action: string; canCheck: boolean }) {
79 const check = useFetcher<Done>();
80 const status = entryStatus(entry);
81 const checking = check.state !== "idle";
82 const rules: { label: string; values: string[] }[] = [
83 { label: "Groups", values: entry.groups.map(groupText) },
84 { label: "Ignored", values: entry.ignore.map(ignoreText) },
85 {
86 label: "Allowed",
87 values: entry.allow.map((rule) =>
88 [rule.dependency, rule.dependencyType, ...rule.updateTypes.map((type) => type.replace("version-update:semver-", ""))]
89 .filter(Boolean)
90 .join(" · "),
91 ),
92 },
93 { label: "Assignees", values: entry.assignees },
94 { label: "Reviewers", values: entry.reviewers },
95 ].filter((rule) => rule.values.length > 0);
96 return (
97 <li className="rounded-lg border border-line p-3">
98 <div className="flex flex-wrap items-start justify-between gap-3">
99 <div className="min-w-0">
100 <p className="flex flex-wrap items-center gap-2 text-sm">
101 <span className="font-mono font-medium text-fg">{entry.ecosystem}</span>
102 <span className="font-mono text-muted wrap-anywhere">{entry.directories.join(", ")}</span>
103 {entry.targetBranch && <span className="font-mono text-xs text-faint">→ {entry.targetBranch}</span>}
104 <Badge tone={status.tone}>{status.label}</Badge>
105 </p>
106 <p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
107 {entry.schedule && (
108 <span className="inline-flex items-center gap-1">
109 <CalendarClock size={12} className="shrink-0" />
110 {entry.schedule}
111 </span>
112 )}
113 {entry.nextRunAt && (
Merge branch 'worktree-agent-ae1299e92e4462012'114 <Hint label={utc(entry.nextRunAt)}>
115 <span>Next check {timeUntil(entry.nextRunAt)}</span>
116 </Hint>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar117 )}
118 <span>
119 {entry.lastCheckedAt ? (
120 <>
121 Last checked <TimeAgo at={entry.lastCheckedAt} />
122 </>
123 ) : (
124 "Not checked yet"
125 )}
126 </span>
127 {entry.supported && <span>Up to {entry.openPullRequestsLimit} open</span>}
128 </p>
129 </div>
130 {entry.supported && canCheck && (
131 <check.Form method="post" action={action} className="shrink-0">
132 <input type="hidden" name="intent" value="check_updates" />
133 <input type="hidden" name="entry" value={entry.id} />
134 <button
135 type="submit"
136 disabled={checking || entry.openPullRequestsLimit === 0}
137 className="inline-flex items-center gap-1.5 rounded-md border border-line px-2.5 py-1 text-xs text-fg/80 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg disabled:opacity-50"
138 >
139 <RefreshCw size={12} className={checking ? "animate-spin" : ""} />
140 {checking ? "Checking…" : "Check for updates"}
141 </button>
142 </check.Form>
143 )}
144 </div>
145 {check.data?.error && <p className="mt-2 text-xs text-danger">{check.data.error}</p>}
146 {entry.lastError ? (
147 <p className="mt-2 flex items-start gap-1.5 text-xs text-danger">
148 <FileWarning size={12} className="mt-0.5 shrink-0" />
149 <span className="wrap-anywhere">
150 <Said text={entry.lastError} />
151 </span>
152 </p>
153 ) : (
154 entry.lastResult && (
155 <p className="mt-2 flex items-start gap-1.5 text-xs text-muted">
Merge branch 'worktree-agent-ad7c6d88d93adc817'156 <CircleCheck size={12} className="mt-0.5 shrink-0 text-success" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar157 <span className="wrap-anywhere">
158 <Said text={entry.lastResult} />
159 </span>
160 </p>
161 )
162 )}
163 {rules.length > 0 && (
164 <dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
165 {rules.map((rule) => (
166 <div key={rule.label} className="contents">
167 <dt className="text-faint">{rule.label}</dt>
168 <dd className="font-mono text-muted wrap-anywhere">{rule.values.join("; ")}</dd>
169 </div>
170 ))}
171 </dl>
172 )}
173 {entry.notes.length > 0 && (
174 <ul className="mt-2 space-y-1">
175 {entry.notes.map((note) => (
176 <li key={note} className="flex items-start gap-1.5 text-xs text-faint">
177 <Info size={12} className="mt-0.5 shrink-0" />
178 <span>
179 <Said text={note} />
180 </span>
181 </li>
182 ))}
183 </ul>
184 )}
185 </li>
186 );
187}
188
189/** One update pull request g1t has open or is making. */
190function PullRow({ pull, base }: { pull: UpdatePull; base: string }) {
191 const icon =
192 pull.state === "needs_code" ? (
193 <Wrench size={13} className="text-warn" />
194 ) : pull.state === "requested" ? (
195 <Loader size={13} className="text-info" />
196 ) : (
Merge branch 'worktree-agent-ad7c6d88d93adc817'197 <GitPullRequest size={13} className="text-success" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar198 );
199 const count = pull.dependencies.length;
200 return (
201 <li className="flex items-start gap-2 py-2">
202 <span className="mt-0.5 shrink-0">{icon}</span>
203 <div className="min-w-0 flex-1">
204 <p className="text-sm">
205 {pull.pull ? (
206 <Link to={`${base}/pulls/${pull.pull}`} className="hover:underline">
207 {pull.title} <span className="text-muted">#{pull.pull}</span>
208 </Link>
209 ) : (
210 pull.title
211 )}
212 </p>
213 <p className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted">
214 <Badge tone={PULL_TONES[pull.state]}>{PULL_STATES[pull.state]}</Badge>
215 <span>{pull.kind === "security" ? "Security update" : "Version update"}</span>
216 <span>
217 {count} {count === 1 ? "dependency" : "dependencies"}
218 </span>
219 {pull.mergeRequestedBy && (
220 <span className="inline-flex items-center gap-1">
221 <GitMerge size={11} />
222 Merges when its checks pass, as @{pull.mergeRequestedBy} asked
223 </span>
224 )}
225 </p>
226 {pull.error && <p className="mt-0.5 text-xs text-danger wrap-anywhere">{pull.error}</p>}
227 </div>
228 </li>
229 );
230}
231
232export function DependencyUpdates({
233 state,
234 action,
235 base,
236 canCheck,
237 titled = true,
238}: {
239 state: VersionUpdatesState;
240 action: string;
241 base: string;
242 canCheck: boolean;
243 /** Its own title and what it is for; off where the page says so above it. */
244 titled?: boolean;
245}) {
246 const pulls = livePulls(state.pulls);
247 const fileLink = state.path && state.commit ? `${base}/blob/${state.commit}/${state.path}` : null;
248 return (
249 <div className="rounded-xl border border-line bg-surface p-4">
250 {titled && (
251 <>
252 <div className="flex flex-wrap items-center justify-between gap-2">
253 <span className="text-sm font-medium">Dependency updates</span>
254 <a
255 href={DEPENDENCY_UPDATES_DOCS}
256 target="_blank"
257 rel="noreferrer"
258 className="inline-flex items-center gap-1 text-xs text-fg-soft hover:text-fg hover:underline"
259 >
260 How to write the file
261 <ExternalLink size={10} />
262 </a>
263 </div>
264 <p className="mt-1 text-sm text-muted">
265 Pull requests that keep dependencies current on a schedule, from a <code className="text-fg-soft">dependabot.yml</code>{" "}
266 file (version 2) in <code className="text-fg-soft">.g1t/</code> or <code className="text-fg-soft">.github/</code>. Security
267 updates follow the same file.
268 </p>
269 </>
270 )}
271
272 <div className={`${titled ? "mt-3 " : ""}flex items-start gap-1.5 text-xs text-muted`}>
273 {state.found && state.path ? (
274 <>
275 {state.problems.length > 0 ? (
276 <FileWarning size={13} className="shrink-0 text-danger" />
277 ) : (
278 <FileCode size={13} className="shrink-0 text-accent" />
279 )}
280 <span>
281 Reading{" "}
282 {fileLink ? (
283 <Link to={fileLink} className="font-mono text-fg-soft hover:underline">
284 {state.path}
285 </Link>
286 ) : (
287 <span className="font-mono text-fg-soft">{state.path}</span>
288 )}
289 {state.readAt && (
290 <>
291 , read <TimeAgo at={state.readAt} />
292 </>
293 )}
294 {state.problems.length === 0 && (
295 <>
296 : {state.updates.length} {state.updates.length === 1 ? "entry" : "entries"}
297 </>
298 )}
299 </span>
300 </>
301 ) : (
302 <>
303 <CircleDot size={13} className="shrink-0 text-faint" />
304 <span>
305 No <code>dependabot.yml</code> on the default branch.
306 </span>
307 </>
308 )}
309 </div>
310 {state.ignoredPaths.length > 0 && (
311 <p className="mt-1 flex items-start gap-1.5 text-xs text-warn">
312 <Info size={12} className="mt-0.5 shrink-0" />
313 <span>
314 Also found {state.ignoredPaths.map((path) => <code key={path}>{path} </code>)}
315 and ignored it: the file under <code>.g1t/</code> is read in its place.
316 </span>
317 </p>
318 )}
319
320 {state.problems.length > 0 && (
321 <div className="mt-3 rounded-lg border border-danger/40 bg-danger/5 p-3">
322 <p className="text-xs font-medium text-danger">
323 {state.problems.length === 1 ? "1 problem" : `${state.problems.length} problems`}. Nothing is updated from this file until
324 {state.problems.length === 1 ? " it is" : " they are"} fixed.
325 </p>
326 <ul className="mt-2 space-y-1 text-xs">
327 {state.problems.map((problem) => (
328 <li key={`${problem.line}:${problem.column}:${problem.key}:${problem.message}`} className="flex gap-2">
329 <span className="w-14 shrink-0 font-mono text-faint tabular-nums">
330 {problem.line > 0 ? `Line ${problem.line}` : "File"}
331 </span>
332 <span className="min-w-0 text-fg/90 wrap-anywhere">
333 {problem.key && <code className="mr-1 text-fg-soft">{problem.key}</code>}
334 <Said text={problem.message} />
335 </span>
336 </li>
337 ))}
338 </ul>
339 </div>
340 )}
341
342 {state.updates.length > 0 && (
343 <section className="mt-4">
344 <Heading>Entries</Heading>
345 <ul className="mt-2 space-y-2">
346 {state.updates.map((entry) => (
347 <Entry key={entry.id} entry={entry} action={action} canCheck={canCheck && state.problems.length === 0} />
348 ))}
349 </ul>
350 </section>
351 )}
352
353 {state.registries.length > 0 && (
354 <section className="mt-4">
355 <Heading>Private registries</Heading>
356 <ul className="mt-2 divide-y divide-line/60 text-xs">
357 {state.registries.map((registry) => (
358 <li key={registry.name} className="flex flex-wrap items-center gap-x-3 gap-y-0.5 py-1.5">
359 <span className="font-mono text-fg-soft">{registry.name}</span>
360 <span className="text-faint">{registry.kind}</span>
361 <span className="min-w-0 font-mono text-muted wrap-anywhere">{registry.url}</span>
362 {registry.secrets.length > 0 && (
363 <span className="inline-flex items-center gap-1 text-muted">
364 <KeyRound size={11} />
365 {registry.secrets.join(", ")}
366 </span>
367 )}
368 </li>
369 ))}
370 </ul>
371 </section>
372 )}
373
374 <section className="mt-4">
375 <Heading>Update pull requests</Heading>
376 {pulls.length > 0 ? (
377 <ul className="mt-1 divide-y divide-line/60">
378 {pulls.map((pull) => (
379 <PullRow key={`${pull.branch}:${pull.updatedAt}`} pull={pull} base={base} />
380 ))}
381 </ul>
382 ) : (
383 <p className="mt-2 text-xs text-muted">None open.</p>
384 )}
385 </section>
386
387 {state.ignores.length > 0 && (
388 <section className="mt-4">
389 <Heading>Ignored in comments</Heading>
390 <ul className="mt-2 space-y-1 text-xs">
391 {state.ignores.map((condition) => (
392 <li
393 key={`${condition.ecosystem}:${condition.dependency}:${conditionText(condition)}`}
394 className="flex flex-wrap items-center gap-x-2 gap-y-0.5"
395 >
396 <EyeOff size={11} className="shrink-0 text-faint" />
397 <span className="font-mono text-fg-soft">{condition.dependency}</span>
398 <span className="text-muted">{conditionText(condition)}</span>
399 <span className="text-faint">
400 {condition.ecosystem} · @{condition.by}
401 {condition.pull != null && (
402 <>
403 {" "}
404 in{" "}
405 <Link to={`${base}/pulls/${condition.pull}`} className="hover:underline">
406 #{condition.pull}
407 </Link>
408 </>
409 )}
410 </span>
411 </li>
412 ))}
413 </ul>
414 <p className="mt-2 text-xs text-faint">
415 Comment <code>@g1t unignore &lt;dependency&gt;</code> on an update pull request to undo one.
416 </p>
417 </section>
418 )}
419 </div>
420 );
421}

This file's history is long; its oldest lines are credited to the oldest commit read.