Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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