Skip to content

g1t/apps/web/app/routes/repo/release-new.tsx

97 lines4,612 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 { Form, data, redirect } from "react-router";
2
3import { MAX_RELEASE_BODY_CHARS, MAX_RELEASE_NAME_CHARS } from "@g1t/contracts";
4
5import type { Route } from "./+types/release-new";
6import { encodeTag } from "../../components/releases";
7import { ErrorText, Field, Input, SubmitButton, Textarea } from "../../components/ui";
8import { requireRepo } from "../../lib/access.server";
9import { page } from "../../lib/meta";
10import { repos } from "../../lib/services.server";
11import { assertSameOrigin, requireUser } from "../../lib/session.server";
12
13export function meta({ params, ...args }: Route.MetaArgs) {
14 return page(args, { title: `New release · ${params.owner}/${params.repo} · g1t` });
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const { viewer, repo } = await requireRepo(context, params, "push");
19 const path = { namespace: params.owner, name: params.repo };
20 const [tags, branches] = await Promise.all([
21 repos.tags(path, viewer).then((found) => (found.ok ? found.value.map((tag) => tag.name) : [])).catch(() => []),
22 repos.branches(path, viewer).then((found) => (found.ok ? found.value.map((branch) => branch.name) : [])).catch(() => []),
23 ]);
24 return { tags, branches, defaultBranch: repo.defaultBranch };
25}
26
27export async function action({ request, params, context }: Route.ActionArgs) {
28 assertSameOrigin(request);
29 const user = requireUser(context, request);
30 const form = await request.formData();
31 const text = (key: string) => String(form.get(key) ?? "").trim();
32 const tagName = text("tag");
33 if (!tagName) return data({ error: "Choose a tag, or name a new one." }, { status: 400 });
34 const created = await repos.createRelease(user, { namespace: params.owner, name: params.repo }, {
35 tagName,
36 target: text("target") || null,
37 name: text("name") || null,
38 body: String(form.get("body") ?? ""),
39 draft: form.get("intent") === "draft",
40 prerelease: form.get("prerelease") === "on",
41 });
42 if (!created.ok) return data({ error: created.error.message }, { status: 400 });
43 throw redirect(`/${params.owner}/${params.repo}/releases/tag/${encodeTag(created.value.tagName)}`);
44}
45
46export default function NewRelease({ loaderData, actionData }: Route.ComponentProps) {
47 const { tags, branches, defaultBranch } = loaderData;
48 return (
49 <div className="mx-auto max-w-3xl space-y-5">
50 <div>
51 <h2 className="text-xl font-semibold tracking-tight">New release</h2>
52 <p className="mt-1 text-sm text-muted">
53 Publish a tag with a title and notes. A tag that does not exist yet is made at the branch or commit you choose.
54 </p>
55 </div>
56 <Form method="post" className="space-y-4 rounded-xl border border-line bg-surface p-5">
57 <div className="grid gap-4 sm:grid-cols-2">
58 <Field label="Tag" hint={tags.length > 0 ? "An existing tag, or a new one such as v1.0.0." : "A new tag, such as v1.0.0."}>
59 <Input name="tag" list="release-tags" required placeholder="v1.0.0" />
60 </Field>
61 <Field label="Target" hint="Where a new tag is made. Ignored for a tag that exists.">
62 <Input name="target" list="release-targets" placeholder={defaultBranch} />
63 </Field>
64 </div>
65 <datalist id="release-tags">
66 {tags.map((tag) => (
67 <option key={tag} value={tag} />
68 ))}
69 </datalist>
70 <datalist id="release-targets">
71 {branches.map((branch) => (
72 <option key={branch} value={branch} />
73 ))}
74 </datalist>
75 <Field label="Title">
76 <Input name="name" maxLength={MAX_RELEASE_NAME_CHARS} placeholder="What this release is" />
77 </Field>
78 <Field label="Notes" hint="Markdown: what changed, and anything people need to do.">
79 <Textarea name="body" rows={12} maxLength={MAX_RELEASE_BODY_CHARS} placeholder={"## What changed\n\n- "} />
80 </Field>
81 <label className="flex items-center gap-2 text-sm">
82 <input type="checkbox" name="prerelease" className="size-4" />
83 Set as a pre-release: not ready for everyone, never the latest
84 </label>
85 {actionData?.error && <ErrorText>{actionData.error}</ErrorText>}
86 <div className="flex flex-wrap items-center gap-2">
87 <SubmitButton name="intent" value="publish" pending="Publishing…">
88 Publish release
89 </SubmitButton>
90 <SubmitButton variant="quiet" name="intent" value="draft" pending="Saving…">
91 Save draft
92 </SubmitButton>
93 </div>
94 </Form>
95 </div>
96 );
97}

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