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.

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