Skip to content

g1t/apps/web/app/routes/repo/deployments.tsx

834 lines35,183 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.

Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971import {
2 ArrowUpRight,
3 ChevronLeft,
4 ChevronRight,
5 ExternalLink,
6 GitBranch,
7 GitCommitHorizontal,
8 Globe,
9 Rocket,
10 RotateCw,
11 Terminal,
12 Trash2,
13} from "lucide-react";
Deployments: a preview for every pull request, production on g1t.page14import type { ReactNode } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'15import { Form, Link } from "react-router";
Deployments: a preview for every pull request, production on g1t.page16
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9717import type { DeploymentEnvironment, FeatureState, RepoDeployment } from "@g1t/contracts";
Deployments: a preview for every pull request, production on g1t.page18
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9719import { host } from "../../components/deploy";
20import { DeploymentStateBadge, DeploymentStateIcon, sourceLabel } from "../../components/deployments-panel";
21import { FilterMenu } from "../../components/labels";
Projects: what a workspace builds and runs, first on every page22
Deployments: a preview for every pull request, production on g1t.page23import type { Route } from "./+types/deployments";
Social cards for every page: og.g1t.sh24import { page } from "../../lib/meta";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9725import { ButtonLink, ComputeNote, ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
26import { Badge } from "../../components/ui/badge";
Merge branch 'worktree-agent-ae1299e92e4462012'27import { Hint } from "../../components/ui/hint";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28import { computeNoteFor } from "../../lib/compute.server";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9729import { neverDeploys } from "../../lib/project-kind";
A project is an app or a library: libraries show their package and how to ship a release, not production30import { billing, deployments, projects } from "../../lib/services.server";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9731import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32import { refusal, requireRepo } from "../../lib/access.server";
33import { whyNot } from "../../lib/access";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9734import {
35 type ListFilter,
36 SOURCES,
37 STATES,
38 STATE_WORD,
39 SOURCE_LABEL,
40 choices,
41 environmentLabel,
42 environmentUrl,
43 isFiltered,
44 orderEnvironments,
45 pageRange,
46 parseFilter,
47 shortSha,
48 withFilter,
49} from "../../lib/deployments";
50
51const API_GUIDE = "https://docs.g1t.sh/guides/deployments-api/";
Deployments: a preview for every pull request, production on g1t.page52
Social cards for every page: og.g1t.sh53export function meta({ params, ...args }: Route.MetaArgs) {
54 return page(args, { title: `Deployments · ${params.owner}/${params.repo} · g1t` });
Deployments: a preview for every pull request, production on g1t.page55}
56
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9757export async function loader({ params, context, request }: Route.LoaderArgs) {
Deployments: a preview for every pull request, production on g1t.page58 const viewer = getViewer(context);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59 // Anyone who can read the repository: a public one's to anyone, a private one's to people with a role.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9760 const { access, repo } = await requireRepo(context, params, "read");
61 const path = { namespace: repo.namespace, name: repo.name };
Projects: what a workspace builds and runs, first on every page62 const ref = { workspace: params.owner, slug: params.repo };
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9763 const filter = parseFilter(new URL(request.url).searchParams);
64 // A part whose service fails shows as missing; the page stays up.
65 const soft = <T,>(promise: Promise<T>): Promise<T | null> =>
66 promise.catch((error) => (console.warn("deployments:", error), null));
67 // g1t.page's settings and builds are the workspace's members' alone.
68 const insider = access.insider;
69 const [environments, list, settings, builds, features, computeNote, project] = await Promise.all([
70 soft(deployments.environments(path, viewer)),
71 soft(deployments.repoDeployments(path, viewer, filter)),
72 insider ? soft(deployments.settings(ref, viewer)) : null,
73 insider ? soft(deployments.list(ref, viewer)) : null,
74 insider ? soft(billing.features(params.owner, viewer)) : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75 // Builds are compute: said before a deploy is refused for it.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9776 insider ? soft(computeNoteFor(params.owner, "deploy")) : null,
77 soft(projects.get(params.owner, params.repo, viewer)),
Deployments: a preview for every pull request, production on g1t.page78 ]);
A project is an app or a library: libraries show their package and how to ship a release, not production79 // Set not to deploy, in General settings: turning them on waits for that to change.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9780 const notDeploying = project?.ok ? neverDeploys(project.value) : false;
Fixes from the page sweep81 // Deployments come with the g1t plan. Someone outside the workspace does
82 // not see its plans; the page works without, and the deployments service
83 // refuses a deploy the plan does not cover.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9784 const plan = features?.ok
Fixes from the page sweep85 ? (features.value.find((state) => state.plan.feature === "plan" || state.plan.feature === "deployments") ?? null)
86 : null;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9787 return {
88 can: access.can,
89 filter,
90 environments: environments?.ok ? { ...environments.value, environments: orderEnvironments(environments.value.environments) } : null,
91 list: list?.ok ? list.value : null,
92 // g1t.page hosting, for the workspace's members: null when it could not be read.
93 pages:
94 settings?.ok && builds?.ok
95 ? { settings: settings.value, live: builds.value.live, plan, computeNote: computeNote ?? null, notDeploying }
96 : null,
97 };
Deployments: a preview for every pull request, production on g1t.page98}
99
100export async function action({ request, params, context }: Route.ActionArgs) {
101 assertSameOrigin(request);
102 const user = requireUser(context, request);
103 const form = await request.formData();
Projects: what a workspace builds and runs, first on every page104 const ref = { workspace: params.owner, slug: params.repo };
Deployments: a preview for every pull request, production on g1t.page105 const intent = form.get("intent");
Projects: what a workspace builds and runs, first on every page106 const branch = form.get("branch") ? String(form.get("branch")) : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look107 // Deploying is compute (Write); turning deployments on or off is a deployment setting (Admin).
108 const refused = await refusal(context, params, intent === "enable" || intent === "disable" ? "manage_integrations" : "run");
109 if (refused) return { error: refused };
Deployments: a preview for every pull request, production on g1t.page110 if (intent === "redeploy") {
Projects: what a workspace builds and runs, first on every page111 const started = await deployments.redeploy(user, ref, branch);
Deployments: a preview for every pull request, production on g1t.page112 return started.ok ? { notice: "Build started." } : { error: started.error.message };
113 }
114 if (intent === "take-down") {
Projects: what a workspace builds and runs, first on every page115 const done = await deployments.takeDown(user, ref, branch);
Deployments: a preview for every pull request, production on g1t.page116 return done.ok ? { notice: "Taken down." } : { error: done.error.message };
117 }
118 if (intent === "enable" || intent === "disable") {
Projects: what a workspace builds and runs, first on every page119 const saved = await deployments.updateSettings(user, ref, { enabled: intent === "enable" });
Deployments: a preview for every pull request, production on g1t.page120 return saved.ok
121 ? { notice: intent === "enable" ? "Deployments are on. Production is building." : "Deployments are off, and every app is down." }
122 : { error: saved.error.message };
123 }
Secrets and variables: one list, rows per environment, for workflows and deployments124 return { error: "Unknown request." };
Deployments: a preview for every pull request, production on g1t.page125}
126
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97127type Loaded = Route.ComponentProps["loaderData"];
128type Pages = NonNullable<Loaded["pages"]>;
129
Deployments: a preview for every pull request, production on g1t.page130export default function RepoDeployments({ loaderData, actionData, params }: Route.ComponentProps) {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97131 const { environments, list, pages, filter, can } = loaderData;
Deployments: a preview for every pull request, production on g1t.page132 const base = `/${params.owner}/${params.repo}`;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97133 const total = environments?.total_count ?? list?.total_count ?? 0;
134 const envs = environments?.environments ?? [];
135 const pagesOn = pages?.settings.enabled ?? false;
136 // Anything to show besides how to start: deployments, or g1t.page hosting turned on.
137 const any = total > 0 || envs.length > 0 || pagesOn || (pages?.live.length ?? 0) > 0;
138 const production = envs.find((env) => env.production_environment && environmentUrl(env));
139 const productionUrl = production ? environmentUrl(production) : null;
140 const planNeeded = pages?.plan != null && !pages.plan.on && !pages.plan.included && pages.live.length === 0;
Deployments: a preview for every pull request, production on g1t.page141
142 return (
143 <div className="mx-auto max-w-5xl">
144 <header className="flex flex-wrap items-start justify-between gap-4">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97145 <div className="min-w-0">
Deployments: a preview for every pull request, production on g1t.page146 <h1 className="flex items-center gap-2 text-xl font-semibold tracking-tight">
147 <Rocket size={18} className="text-accent" />
148 Deployments
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97149 {total > 0 && (
150 <span className="rounded-full bg-line px-2 py-px text-xs font-medium tabular-nums text-muted">{total.toLocaleString("en-US")}</span>
151 )}
Deployments: a preview for every pull request, production on g1t.page152 </h1>
153 <p className="mt-1 max-w-2xl text-sm text-muted">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97154 Where this repository runs, and every deployment to it: builds on g1t.page, g1t Actions jobs with an environment,
155 and deployments reported from any other CI.
Deployments: a preview for every pull request, production on g1t.page156 </p>
157 </div>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97158 {productionUrl && (
Deployments: a preview for every pull request, production on g1t.page159 <a
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97160 href={productionUrl}
161 className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-line px-3 py-1.5 font-mono text-xs text-muted hover:border-line-strong hover:text-fg"
Deployments: a preview for every pull request, production on g1t.page162 >
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97163 <Globe size={13} className="shrink-0" />
164 <span className="truncate">{host(productionUrl)}</span>
Deployments: a preview for every pull request, production on g1t.page165 </a>
166 )}
167 </header>
168
169 <div className="mt-4 min-h-6">
Merge branch 'worktree-agent-ad7c6d88d93adc817'170 {actionData && "notice" in actionData && <p className="text-sm text-success">{actionData.notice}</p>}
Deployments: a preview for every pull request, production on g1t.page171 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97172 {!actionData && pages && <ComputeNote note={pages.computeNote} />}
Deployments: a preview for every pull request, production on g1t.page173 </div>
174
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97175 {!environments && !list ? (
176 <p className="rounded-xl border border-line bg-surface p-6 text-sm text-muted">
177 Deployments could not be loaded just now. Reload the page to try again.
178 </p>
179 ) : !any ? (
180 <Start base={base} pages={pages} can={can} owner={params.owner} planNeeded={planNeeded} />
181 ) : (
182 <>
183 {envs.length > 0 && (
184 <section aria-labelledby="environments">
185 <h2 id="environments" className="text-sm font-semibold">
186 Environments <span className="font-normal text-faint">{envs.length}</span>
187 </h2>
188 <ul className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
189 {envs.map((env) => (
190 <EnvironmentCard key={env.name} env={env} base={base} />
191 ))}
192 </ul>
193 </section>
194 )}
195
196 <History base={base} list={list} filter={filter} environments={envs} />
197 </>
198 )}
199
200 {/* g1t.page hosting: its own section once there is anything else to show. */}
201 {any && pages && (
202 <section aria-labelledby="g1t-page" className="mt-12 border-t border-line pt-8">
203 <h2 id="g1t-page" className="flex items-center gap-2 text-sm font-semibold">
204 <Globe size={14} className="text-faint" />
205 On g1t.page
206 </h2>
207 {planNeeded ? (
208 <PlanNeeded plan={pages.plan} owner={params.owner} quiet />
209 ) : !pagesOn ? (
210 <PagesOff pages={pages} base={base} can={can} quiet />
A project is an app or a library: libraries show their package and how to ship a release, not production211 ) : (
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97212 <PagesOn pages={pages} base={base} can={can} />
A project is an app or a library: libraries show their package and how to ship a release, not production213 )}
Deployments: a preview for every pull request, production on g1t.page214 </section>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97215 )}
216 </div>
217 );
218}
219
220/** Nothing has deployed yet: the two ways to start. */
221function Start({
222 base,
223 pages,
224 can,
225 owner,
226 planNeeded,
227}: {
228 base: string;
229 pages: Pages | null;
230 can: Loaded["can"];
231 owner: string;
232 planNeeded: boolean;
233}) {
234 return (
235 <section className="rounded-xl border border-line bg-surface">
236 <div className="border-b border-line px-6 py-5">
237 <h2 className="font-medium">No deployments yet</h2>
238 <p className="mt-1 max-w-2xl text-sm text-muted">
239 Host it here on g1t.page, or deploy it wherever it runs and report each deployment. Either way they show here, by
240 environment.
241 </p>
242 </div>
243 <div className="grid divide-y divide-line md:grid-cols-2 md:divide-x md:divide-y-0">
244 <div className="min-w-0 p-6">
245 <h3 className="flex items-center gap-2 text-sm font-medium">
246 <Globe size={14} className="text-accent" />
247 Host it on g1t.page
248 </h3>
249 {!pages ? (
250 <p className="mt-1.5 text-sm text-muted">
251 Members of the workspace can turn on hosting: production from the default branch, and a live preview for every
252 pull request.
253 </p>
254 ) : planNeeded ? (
255 <PlanNeeded plan={pages.plan} owner={owner} quiet />
256 ) : (
257 <PagesOff pages={pages} base={base} can={can} />
258 )}
259 </div>
260 <div className="min-w-0 p-6">
261 <h3 className="flex items-center gap-2 text-sm font-medium">
262 <Terminal size={14} className="text-accent" />
263 Report deployments from your CI
264 </h3>
265 <p className="mt-1.5 text-sm text-muted">
266 A g1t Actions job with an <code className="font-mono text-fg">environment:</code> reports its deployment by
267 itself. From any other CI, create a deployment and its statuses with the API.
268 </p>
269 <pre className="mt-3 overflow-x-auto rounded-lg border border-line bg-bg p-3 font-mono text-xs leading-relaxed text-muted">
270 {"jobs:\n deploy:\n environment:\n name: production\n url: https://example.com"}
271 </pre>
272 <a href={API_GUIDE} className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline">
273 Reporting deployments <ArrowUpRight size={13} />
274 </a>
275 </div>
276 </div>
277 </section>
278 );
279}
280
281function EnvironmentCard({ env, base }: { env: DeploymentEnvironment; base: string }) {
282 const latest = env.latest;
283 const current = env.current;
284 const url = environmentUrl(env);
285 // What it serves, when a newer deployment has not (yet) replaced it.
286 const serving = current && latest && current.id !== latest.id ? current : null;
287 const filtered = `${base}/deployments?environment=${encodeURIComponent(env.name)}#history`;
288 return (
289 <li className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-4">
290 <div className="flex min-w-0 items-center gap-2">
291 <Link to={filtered} className="min-w-0 truncate font-medium hover:text-accent">
292 {environmentLabel(env.name)}
293 </Link>
294 {env.production_environment && env.name !== "production" && <Badge tone="accent">Production</Badge>}
295 {env.transient_environment && <Badge>Transient</Badge>}
296 <span className="grow" />
297 {latest && <DeploymentStateBadge state={latest.state} />}
298 </div>
299 {url ? (
300 <a href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
301 <span className="truncate">{host(url)}</span>
302 <ExternalLink size={12} className="shrink-0" />
303 </a>
Deployments: a preview for every pull request, production on g1t.page304 ) : (
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97305 <p className="mt-2 text-sm text-faint">No address</p>
306 )}
307 {latest && (
308 <dl className="mt-3 space-y-1.5 text-xs text-muted">
309 <div className="flex min-w-0 items-center gap-2">
310 <dt className="sr-only">Commit</dt>
311 <dd className="flex min-w-0 items-center gap-3">
312 <Link to={`${base}/commit/${latest.sha}`} className="inline-flex shrink-0 items-center gap-1 font-mono hover:text-fg">
313 <GitCommitHorizontal size={13} className="text-faint" />
314 {shortSha(latest.sha)}
315 </Link>
316 <span className="inline-flex min-w-0 items-center gap-1 font-mono">
317 <GitBranch size={12} className="shrink-0 text-faint" />
318 <span className="truncate">{latest.ref}</span>
319 </span>
320 </dd>
321 </div>
322 <div className="flex min-w-0 items-center gap-1">
323 <dt className="sr-only">Deployed by</dt>
324 <dd className="min-w-0 truncate">
325 <Made deployment={latest} base={base} /> · <TimeAgo at={latest.updated_at} />
326 </dd>
327 </div>
328 {serving && (
329 <div className="flex min-w-0 items-center gap-1">
330 <dt className="sr-only">Serving</dt>
331 <dd className="min-w-0 truncate text-faint">
332 Serving{" "}
333 <Link to={`${base}/deployments/${serving.id}`} className="font-mono hover:text-fg">
334 {shortSha(serving.sha)}
335 </Link>{" "}
336 since <TimeAgo at={serving.updated_at} />
337 </dd>
Deployments: a preview for every pull request, production on g1t.page338 </div>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97339 )}
340 </dl>
341 )}
342 <div className="mt-auto flex items-center gap-3 pt-3 text-xs">
343 <Link to={filtered} className="text-muted hover:text-fg">
344 {env.deployments_count.toLocaleString("en-US")} {env.deployments_count === 1 ? "deployment" : "deployments"}
345 </Link>
346 {latest && (
347 <Link to={`${base}/deployments/${latest.id}`} className="ml-auto inline-flex items-center gap-0.5 text-muted hover:text-fg">
348 Latest
349 <ChevronRight size={13} />
350 </Link>
351 )}
352 </div>
353 </li>
354 );
355}
Deployments: a preview for every pull request, production on g1t.page356
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97357/** Who made a deployment and with what: a person or g1t, and g1t Actions (its run), g1t.page or the API. */
358function Made({ deployment, base, link = true }: { deployment: RepoDeployment; base: string; link?: boolean }) {
359 const label = sourceLabel(deployment.source);
360 return (
361 <>
362 <span className="text-fg-soft">{deployment.creator}</span> via{" "}
363 {link && deployment.source === "actions" && deployment.run_url ? (
364 <Link to={deployment.run_url} className="underline-offset-2 hover:text-fg hover:underline">
365 {label}
366 </Link>
367 ) : link && deployment.source === "g1t_page" ? (
368 <Link to={`${base}/deployments#g1t-page`} className="underline-offset-2 hover:text-fg hover:underline">
369 {label}
370 </Link>
371 ) : (
372 label
373 )}
374 </>
375 );
376}
377
378function History({
379 base,
380 list,
381 filter,
382 environments,
383}: {
384 base: string;
385 list: Loaded["list"];
386 filter: ListFilter;
387 environments: DeploymentEnvironment[];
388}) {
389 const path = `${base}/deployments`;
390 const rows = list?.deployments ?? [];
391 // The choices of creators and refs: those seen on this page and as each environment's latest.
392 const seen = [...rows, ...environments.flatMap((env) => [env.latest, env.current])];
393 const envNames = environments.map((env) => env.name);
394 if (filter.environment && !envNames.includes(filter.environment)) envNames.push(filter.environment);
395 const total = list?.total_count ?? 0;
396 const pageCount = list ? Math.max(1, Math.ceil(total / list.per_page)) : 1;
397 return (
398 <section id="history" aria-labelledby="history-heading" className="mt-10 scroll-mt-20">
399 <h2 id="history-heading" className="text-sm font-semibold">
400 History
401 </h2>
402 <div className="mt-3 flex flex-wrap items-center gap-2">
403 <FilterMenu
404 label="Environment"
405 active={filter.environment ? environmentLabel(filter.environment) : undefined}
406 clearTo={filter.environment ? withFilter(path, filter, "environment", null) : undefined}
407 searchPlaceholder="Filter environments"
408 emptyText="No environment matches."
409 options={envNames.map((name) => ({
410 key: name,
411 to: withFilter(path, filter, "environment", name),
412 keywords: name,
413 selected: name === filter.environment,
414 label: <span className="truncate">{environmentLabel(name)}</span>,
415 }))}
416 />
417 <FilterMenu
418 label="State"
419 active={filter.state ? STATE_WORD[filter.state] : undefined}
420 clearTo={filter.state ? withFilter(path, filter, "state", null) : undefined}
421 searchPlaceholder="Filter states"
422 emptyText="No state matches."
423 options={STATES.map((state) => ({
424 key: state,
425 to: withFilter(path, filter, "state", state),
426 keywords: `${STATE_WORD[state]} ${state}`,
427 selected: state === filter.state,
428 label: (
429 <>
430 <DeploymentStateIcon state={state} size={14} />
431 {STATE_WORD[state]}
432 </>
433 ),
434 }))}
435 />
436 <FilterMenu
437 label="Source"
438 active={filter.source ? SOURCE_LABEL[filter.source] : undefined}
439 clearTo={filter.source ? withFilter(path, filter, "source", null) : undefined}
440 searchPlaceholder="Filter sources"
441 emptyText="No source matches."
442 options={SOURCES.map((source) => ({
443 key: source,
444 to: withFilter(path, filter, "source", source),
445 keywords: `${SOURCE_LABEL[source]} ${source}`,
446 selected: source === filter.source,
447 label: SOURCE_LABEL[source],
448 }))}
449 />
450 <FilterMenu
451 label="Creator"
452 active={filter.creator ?? undefined}
453 clearTo={filter.creator ? withFilter(path, filter, "creator", null) : undefined}
454 searchPlaceholder="Filter creators"
455 emptyText="No creator matches."
456 options={choices(seen, "creator", filter.creator).map((creator) => ({
457 key: creator,
458 to: withFilter(path, filter, "creator", creator),
459 keywords: creator,
460 selected: creator === filter.creator,
461 label: <span className="truncate">{creator}</span>,
462 }))}
463 />
464 <FilterMenu
465 label="Ref"
466 active={filter.ref ?? undefined}
467 clearTo={filter.ref ? withFilter(path, filter, "ref", null) : undefined}
468 searchPlaceholder="Filter refs"
469 emptyText="No ref matches."
470 options={choices(seen, "ref", filter.ref).map((ref) => ({
471 key: ref,
472 to: withFilter(path, filter, "ref", ref),
473 keywords: ref,
474 selected: ref === filter.ref,
475 label: <span className="truncate font-mono text-xs">{ref}</span>,
476 }))}
477 />
478 {isFiltered(filter) && (
479 <Link to={`${path}#history`} className="text-xs text-muted hover:text-fg">
480 Clear filters
481 </Link>
482 )}
483 </div>
484
485 <div className="mt-3">
486 {!list ? (
487 <p className="rounded-xl border border-line bg-surface p-6 text-sm text-muted">
488 The history could not be loaded just now. Reload the page to try again.
489 </p>
490 ) : rows.length === 0 ? (
491 <div className="rounded-lg border border-dashed border-line px-6 py-12 text-center">
492 {isFiltered(filter) ? (
493 <>
494 <p className="font-medium">No deployments match</p>
495 <p className="mt-1.5 text-sm text-muted">
496 Try fewer filters, or{" "}
497 <Link to={`${path}#history`} className="text-fg hover:underline">
498 see every deployment
499 </Link>
500 .
501 </p>
502 </>
503 ) : filter.page > 1 ? (
504 <>
505 <p className="font-medium">No deployments on this page</p>
506 <p className="mt-1.5 text-sm text-muted">
507 <Link to={`${path}#history`} className="text-fg hover:underline">
508 Go to the first page
509 </Link>
510 .
511 </p>
512 </>
Deployments: a preview for every pull request, production on g1t.page513 ) : (
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97514 <>
515 <p className="font-medium">No deployments yet</p>
516 <p className="mt-1.5 text-sm text-muted">
517 Push to the default branch to build production on g1t.page, or{" "}
518 <a href={API_GUIDE} className="text-fg hover:underline">
519 report a deployment
520 </a>{" "}
521 from your CI.
522 </p>
523 </>
Deployments: a preview for every pull request, production on g1t.page524 )}
525 </div>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97526 ) : (
527 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
528 {rows.map((deployment) => (
529 <HistoryRow key={deployment.id} deployment={deployment} base={base} />
530 ))}
531 </ul>
532 )}
533 </div>
534
535 {list && total > 0 && (
536 <nav aria-label="Pages" className="mt-3 flex items-center gap-3 text-xs text-muted">
537 <span className="tabular-nums">{pageRange(list.page, list.per_page, total)}</span>
538 <span className="grow" />
539 {list.page > 1 ? (
540 <ButtonLink variant="quiet" to={withFilter(path, filter, "page", list.page - 1)}>
541 <ChevronLeft size={14} />
542 Previous
543 </ButtonLink>
544 ) : (
545 <span className="inline-flex items-center gap-2 rounded-md border border-line px-3.5 py-2 text-sm font-medium opacity-50">
546 <ChevronLeft size={14} />
547 Previous
548 </span>
549 )}
550 {list.page < pageCount ? (
551 <ButtonLink variant="quiet" to={withFilter(path, filter, "page", list.page + 1)}>
552 Next
553 <ChevronRight size={14} />
554 </ButtonLink>
555 ) : (
556 <span className="inline-flex items-center gap-2 rounded-md border border-line px-3.5 py-2 text-sm font-medium opacity-50">
557 Next
558 <ChevronRight size={14} />
559 </span>
560 )}
561 </nav>
562 )}
563 </section>
564 );
565}
566
567function HistoryRow({ deployment, base }: { deployment: RepoDeployment; base: string }) {
568 return (
569 <li>
570 <Link
571 to={`${base}/deployments/${deployment.id}`}
572 className="flex items-start gap-3 px-4 py-3 text-sm transition-colors hover:bg-surface"
573 >
574 <span className="mt-0.5">
575 <DeploymentStateIcon state={deployment.state} size={16} />
576 </span>
577 <span className="min-w-0 grow">
578 <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5">
579 <span className="font-medium">{environmentLabel(deployment.environment)}</span>
580 <span className={`text-xs ${deployment.state === "failure" || deployment.state === "error" ? "text-danger" : "text-muted"}`}>
581 {STATE_WORD[deployment.state]}
582 </span>
583 {deployment.task !== "deploy" && <Badge>{deployment.task}</Badge>}
584 </span>
585 <span className="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-muted">
586 <span className="inline-flex min-w-0 items-center gap-1 font-mono">
587 <GitBranch size={12} className="shrink-0 text-faint" />
588 <span className="truncate">{deployment.ref}</span>
589 </span>
590 <span className="shrink-0 font-mono text-faint">{shortSha(deployment.sha)}</span>
591 {deployment.description && <span className="hidden min-w-0 truncate sm:inline">· {deployment.description}</span>}
592 </span>
593 {deployment.description && <span className="mt-0.5 block truncate text-xs text-muted sm:hidden">{deployment.description}</span>}
594 {/* Who and when, under the rest on a narrow screen. */}
595 <span className="mt-0.5 block truncate text-xs text-faint sm:hidden">
596 <Made deployment={deployment} base={base} link={false} /> · <TimeAgo at={deployment.created_at} />
597 </span>
598 </span>
599 <span className="hidden shrink-0 text-right text-xs text-faint sm:block">
600 <span className="block">
601 <Made deployment={deployment} base={base} link={false} />
602 </span>
603 <span className="mt-0.5 block">
604 <TimeAgo at={deployment.created_at} />
605 </span>
606 </span>
607 </Link>
608 </li>
609 );
610}
Deployments: a preview for every pull request, production on g1t.page611
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97612function PagesOn({ pages, base, can }: { pages: Pages; base: string; can: Loaded["can"] }) {
613 const { settings, live } = pages;
614 const production = live.find((app) => app.kind === "production");
615 const previews = live.filter((app) => app.kind === "preview");
616 return (
617 <>
618 <p className="mt-1 max-w-2xl text-sm text-muted">
619 Every pull request gets a live preview on g1t.page, and the default branch goes to production on each push. Apps run
620 only while someone visits them.{" "}
621 <a href="https://docs.g1t.sh/guides/deployments/" className="text-fg hover:underline">
622 How it works
623 </a>
624 </p>
625 <div className="mt-4 grid gap-4 md:grid-cols-2">
626 <LiveCard
627 title="Production"
628 hint="The default branch, on every push."
629 app={production}
630 url={production && settings.primaryDomain ? `https://${settings.primaryDomain}` : undefined}
631 off={!settings.production}
632 actions={can.run ? <AppActions branch={null} up={!!production} /> : null}
633 />
634 <div className="min-w-0 rounded-xl border border-line bg-surface p-5">
635 <h3 className="text-sm font-medium">Previews</h3>
636 <p className="mt-0.5 text-xs text-faint">
637 {settings.previews
638 ? `One per branch with an open pull request; down when it closes or after ${settings.idleDays} days without a visit.`
639 : "Off for this repository."}
Secrets and variables: one list, rows per environment, for workflows and deployments640 </p>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97641 {previews.length === 0 ? (
642 <p className="mt-4 text-sm text-muted">No previews are up.</p>
643 ) : (
644 <ul className="mt-3 divide-y divide-line">
645 {previews.map((app) => (
646 <li key={app.url} className="flex min-w-0 items-center gap-3 py-2 text-sm">
647 <span className="min-w-0 shrink truncate">
648 <span className="font-medium">{app.branch}</span>
649 {app.number != null && (
650 <Link to={`${base}/pull/${app.number}`} className="ml-1.5 text-xs text-muted hover:underline">
651 #{app.number}
652 </Link>
653 )}
654 </span>
655 <a href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block">
656 {host(app.url)}
657 </a>
658 <span className="ml-auto shrink-0">{can.run && <AppActions branch={app.branch} up compact />}</span>
659 </li>
660 ))}
661 </ul>
662 )}
663 </div>
664 </div>
665 <p className="mt-4 text-sm text-muted">
666 Its builds are in the history above, under{" "}
667 <Link to={`${base}/deployments?source=g1t_page#history`} className="text-fg hover:underline">
668 g1t.page
669 </Link>
670 . Build command, output directory, idle days, and turning hosting off are under{" "}
671 <Link to={`${base}/settings/deployments`} className="text-fg hover:underline">
672 Settings → Deployments
673 </Link>
674 . Secrets and config for builds and running apps are under{" "}
675 <Link to={`${base}/settings/secrets`} className="text-fg hover:underline">
676 Settings → Secrets and variables
677 </Link>
678 , and production's own domains under{" "}
679 <Link to={`${base}/settings/domains`} className="text-fg hover:underline">
680 Settings → Domains
681 </Link>
682 .
683 </p>
684 </>
685 );
686}
687
688/** g1t.page hosting is off: what turning it on does, and the button. */
689function PagesOff({ pages, base, can, quiet }: { pages: Pages; base: string; can: Loaded["can"]; quiet?: boolean }) {
690 return (
691 <>
692 <p className="mt-1.5 max-w-2xl text-sm text-muted">
693 {quiet ? "This repository can also be hosted here. " : ""}
694 Once on, production goes up at <span className="font-mono text-fg [overflow-wrap:anywhere]">{host(pages.settings.productionUrl)}</span>{" "}
695 and every open pull request gets its own preview. Workers projects (a{" "}
696 <code className="font-mono text-fg">wrangler.jsonc</code>) and static sites deploy without configuration.
697 </p>
698 {pages.notDeploying ? (
699 <p className="mt-4 max-w-2xl text-sm text-muted">
700 This project is set to be something that doesn't deploy here, such as a library, a tool or an app deployed
701 elsewhere. To host it on g1t.page, choose App or site, deployed on g1t, in{" "}
702 <Link to={`${base}/settings#kind`} className="text-fg underline underline-offset-4">
703 its settings
704 </Link>{" "}
705 first.
706 </p>
707 ) : (
708 <Form method="post" className="mt-4">
709 <Hint label={whyNot(can, "manage_integrations")} disabled={!can.manage_integrations}>
710 <SubmitButton
711 variant={quiet ? "quiet" : "accent"}
712 name="intent"
713 value="enable"
714 pending="Turning on…"
715 disabled={!can.manage_integrations}
716 >
717 <Rocket size={14} />
718 Turn on g1t.page hosting
719 </SubmitButton>
720 </Hint>
721 </Form>
Deployments: a preview for every pull request, production on g1t.page722 )}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97723 </>
Deployments: a preview for every pull request, production on g1t.page724 );
725}
726
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97727function PlanNeeded({ plan, owner, quiet }: { plan: FeatureState | null; owner: string; quiet?: boolean }) {
Deployments: a preview for every pull request, production on g1t.page728 return (
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97729 <div className={quiet ? "mt-1.5" : "mt-2 rounded-xl border border-accent/30 bg-accent/5 p-6"}>
730 {!quiet && <h2 className="font-medium">Deployments are part of a paid plan</h2>}
731 <p className="max-w-2xl text-sm text-muted">
732 Hosting on g1t.page is part of a paid plan. Turn on Deployments for the {owner} workspace and every project in it can
733 have a preview for each pull request and production on g1t.page.
Deployments: a preview for every pull request, production on g1t.page734 {plan && ` $${(plan.plan.monthlyCents / 100).toFixed(0)} a month, including:`}
735 </p>
736 {plan && (
737 <ul className="mt-3 grid gap-1.5 text-sm text-muted sm:grid-cols-2">
738 {plan.plan.includes.map((line) => (
739 <li key={line} className="flex gap-2">
Merge branch 'worktree-agent-ad7c6d88d93adc817'740 <span className="text-success">✓</span>
Deployments: a preview for every pull request, production on g1t.page741 {line}
742 </li>
743 ))}
744 </ul>
745 )}
746 {plan && <p className="mt-3 text-xs text-faint">{plan.plan.overage}</p>}
747 <div className="mt-4">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97748 <ButtonLink variant={quiet ? "quiet" : "accent"} to={`/${owner}/-/billing`}>
Deployments: a preview for every pull request, production on g1t.page749 See the plan under Billing
750 </ButtonLink>
751 </div>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97752 </div>
Deployments: a preview for every pull request, production on g1t.page753 );
754}
755
756function LiveCard({
757 title,
758 hint,
759 app,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97760 url,
Deployments: a preview for every pull request, production on g1t.page761 off,
762 actions,
763}: {
764 title: string;
765 hint: string;
766 app: { url: string; commit: string; deployedAt: string } | undefined;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97767 /** The project's own domain, when it has one. */
768 url?: string;
Deployments: a preview for every pull request, production on g1t.page769 off: boolean;
770 actions: ReactNode;
771}) {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97772 const href = url ?? app?.url;
Deployments: a preview for every pull request, production on g1t.page773 return (
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97774 <div className="min-w-0 rounded-xl border border-line bg-surface p-5">
775 <h3 className="text-sm font-medium">{title}</h3>
Deployments: a preview for every pull request, production on g1t.page776 <p className="mt-0.5 text-xs text-faint">{off ? "Off for this repository." : hint}</p>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97777 {app && href ? (
Deployments: a preview for every pull request, production on g1t.page778 <>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97779 <a href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
780 <span className="truncate">{host(href)}</span>
781 <ExternalLink size={12} className="shrink-0" />
Deployments: a preview for every pull request, production on g1t.page782 </a>
783 <p className="mt-1 text-xs text-faint">
784 <span className="font-mono">{app.commit.slice(0, 8)}</span> · deployed <TimeAgo at={app.deployedAt} />
785 </p>
786 </>
787 ) : (
788 <p className="mt-4 text-sm text-muted">Not up.</p>
789 )}
790 {!off && <div className="mt-4">{actions}</div>}
791 </div>
792 );
793}
794
Merge branch 'worktree-agent-a985c01f43538af01'795function AppActions({ branch, up, compact }: { branch: string | null; up: boolean; compact?: boolean }) {
796 // Which app's buttons say they are working: production has no branch.
797 const app = branch ?? "production";
Deployments: a preview for every pull request, production on g1t.page798 return (
799 <Form method="post" className="flex items-center gap-2">
Projects: what a workspace builds and runs, first on every page800 {branch != null && <input type="hidden" name="branch" value={branch} />}
Merge branch 'worktree-agent-a985c01f43538af01'801 <input type="hidden" name="app" value={app} />
Merge branch 'worktree-agent-ae1299e92e4462012'802 <Hint label="Build again from the current head">
Merge branch 'worktree-agent-a985c01f43538af01'803 <SubmitButton
804 variant="quiet"
805 name="intent"
Merge branch 'worktree-agent-ae1299e92e4462012'806 value="redeploy"
Merge branch 'worktree-agent-a985c01f43538af01'807 match={{ app }}
808 icon={compact}
Merge branch 'worktree-agent-ae1299e92e4462012'809 pending="Redeploying…"
810 aria-label={compact ? "Redeploy" : undefined}
Merge branch 'worktree-agent-a985c01f43538af01'811 >
Merge branch 'worktree-agent-ae1299e92e4462012'812 <RotateCw size={13} />
813 {!compact && "Redeploy"}
Merge branch 'worktree-agent-a985c01f43538af01'814 </SubmitButton>
Merge branch 'worktree-agent-ae1299e92e4462012'815 </Hint>
816 {up && (
817 <Hint label="Take it down now">
818 <SubmitButton
819 variant="quiet"
820 name="intent"
821 value="take-down"
822 match={{ app }}
823 icon={compact}
824 pending="Taking down…"
825 aria-label={compact ? "Take down" : undefined}
826 >
827 <Trash2 size={13} />
828 {!compact && "Take down"}
829 </SubmitButton>
830 </Hint>
Deployments: a preview for every pull request, production on g1t.page831 )}
832 </Form>
833 );
834}

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