Skip to content

g1t/apps/web/app/routes/repo/settings-actions.tsx

166 lines7,837 bytesCodeBlameRaw

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 'worktree-agent-a3abfcce648e87dca'1import { Form, Link } from "react-router";
2
3import { APPROVAL_POLICIES } from "@g1t/contracts";
4import type { ActionsSettingsChange, ApprovalPolicy } from "@g1t/contracts";
5
6import type { Route } from "./+types/settings-actions";
7import { RepoSettingsHeading } from "../../components/repo-settings-heading";
8import { SettingsSection as Section } from "../../components/settings-section";
9import { ErrorText, SubmitButton } from "../../components/ui";
10import { CheckboxOption } from "../../components/ui/checkbox";
11import { RadioGroup, RadioOption } from "../../components/ui/radio-group";
12import { page } from "../../lib/meta";
13import { actions } from "../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
15import { requireCapability, requireInsider } from "../../lib/access.server";
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Actions · ${params.owner}/${params.repo} · g1t` });
19}
20
21export async function loader({ params, context }: Route.LoaderArgs) {
22 // Admins; to anyone without a role here the page does not exist.
23 await requireInsider(context, params, "manage_integrations");
24 const settings = await actions.actionsSettings({ namespace: params.owner, name: params.repo }, getViewer(context));
25 return { settings: unwrap(settings) };
26}
27
28export async function action({ request, params, context }: Route.ActionArgs) {
29 assertSameOrigin(request);
30 const user = requireUser(context, request);
31 await requireCapability(context, params, "manage_integrations");
32 const form = await request.formData();
33 const chosen = String(form.get("defaultPermissions"));
34 const permissions = chosen === "write" || chosen === "inherit" ? chosen : "read";
35 const policy = String(form.get("approvalPolicy"));
36 const change: ActionsSettingsChange = { defaultPermissions: permissions };
37 // Only where the workspace allows it is the box there to send.
38 if (form.has("pullRequestsShown")) change.canApprovePullRequests = form.get("canApprovePullRequests") === "on";
39 if ((APPROVAL_POLICIES as readonly string[]).includes(policy)) change.approvalPolicy = policy as ApprovalPolicy;
40 const saved = await actions.setActionsSettings(user, { namespace: params.owner, name: params.repo }, change);
41 return saved.ok ? { saved: true, error: null } : { saved: false, error: saved.error.message };
42}
43
44/** Each approval policy, least strict first, and whose runs it holds. */
45const POLICY_WORDS: Record<ApprovalPolicy, { label: string; about: string }> = {
46 first_time_contributors: {
47 label: "First-time contributors",
48 about: "Someone outside the workspace who has not had a pull request merged here.",
49 },
50 outside_contributors: {
51 label: "Outside contributors",
52 about: "Those, and everyone outside the workspace who cannot push here: pull requests from forks, and from people with Read or Triage.",
53 },
54 all_external_contributors: {
55 label: "All external contributors",
56 about: "Everyone outside the workspace, outside collaborators with Write included.",
57 },
58};
59
60export default function RepoActionsSettings({ loaderData, actionData, params }: Route.ComponentProps) {
61 const base = `/${params.owner}/${params.repo}`;
62 const { settings } = loaderData;
63 return (
64 <>
65 <RepoSettingsHeading base={base} />
66 <Form method="post" className="max-w-4xl space-y-8">
67 <Section
68 title="Workflow permissions"
69 about={
70 <>
71 What the token of a job without <code className="font-mono text-xs">permissions:</code> can do. Workflows and
72 jobs that write <code className="font-mono text-xs">permissions:</code> get what they write.
73 </>
74 }
75 >
76 <RadioGroup
77 name="defaultPermissions"
78 defaultValue={settings.defaultChosen ? settings.defaultPermissions : "inherit"}
79 className="gap-3"
80 >
81 <RadioOption
82 value="inherit"
83 label="As the workspace says"
84 description={`Now ${settings.defaultPermissions === "write" ? "read and write" : "read-only"}: the workspace's default for new repositories, or read and write for a repository made before restricted tokens.`}
85 />
86 <RadioOption
87 value="read"
88 label="Read repository contents and packages"
89 description={
90 <>
91 <code className="font-mono">contents: read</code> and <code className="font-mono">packages: read</code>.
92 </>
93 }
94 />
95 <RadioOption
96 value="write"
97 label="Read and write"
98 disabled={settings.maxPermissions === "read"}
99 description={
100 settings.maxPermissions === "read"
101 ? "The workspace holds its repositories to read-only."
102 : "Read and write to everything a job's token can reach in this repository."
103 }
104 />
105 </RadioGroup>
106 <p className="text-sm text-muted">
107 Whatever a workflow asks for, a pull request from outside the repository's writers gets a token that can only
108 read.
109 </p>
110 <input type="hidden" name="pullRequestsShown" value={settings.workspaceAllowsPullRequests ? "1" : ""} disabled={!settings.workspaceAllowsPullRequests} />
111 <CheckboxOption
112 name="canApprovePullRequests"
113 defaultChecked={settings.canApprovePullRequests}
114 disabled={!settings.workspaceAllowsPullRequests}
115 label="Allow g1t Actions to create and approve pull requests"
116 description={
117 settings.workspaceAllowsPullRequests
118 ? "Jobs' tokens may open pull requests and approve them. Off unless you turn it on."
119 : "The workspace does not allow it: an owner can, in the workspace's Actions settings."
120 }
121 />
122 </Section>
123
124 <Section
125 title="Approval for pull requests from outside"
126 about={
127 <>
128 Whose pull requests' runs wait as <span className="text-fg/85">Approval required</span> until someone with the
129 Write role approves them. Nothing runs before then, and no token or secret is handed out.
130 </>
131 }
132 >
133 <RadioGroup name="approvalPolicy" defaultValue={settings.approvalPolicy} className="gap-3">
134 {APPROVAL_POLICIES.map((policy) => (
135 <RadioOption
136 key={policy}
137 value={policy}
138 label={
139 <>
140 {POLICY_WORDS[policy].label}
141 {policy === "outside_contributors" && <span className="text-muted">(the default)</span>}
142 </>
143 }
144 description={POLICY_WORDS[policy].about}
145 />
146 ))}
147 </RadioGroup>
148 <p className="text-sm text-muted">
149 Members' pull requests never wait, nor does g1t's own work. Each new push to a pull request that waits, waits
150 again. See{" "}
151 <Link to={`${base}/settings/environments`} className="text-fg underline-offset-2 hover:underline">
152 Environments
153 </Link>{" "}
154 to make deployments wait for a review.
155 </p>
156 </Section>
157
158 <div className="sticky bottom-0 -mx-4 flex flex-wrap items-center gap-4 border-t border-line bg-bg/90 px-4 py-4 backdrop-blur">
159 <SubmitButton pending="Saving…">Save settings</SubmitButton>
160 {actionData?.saved && <span className="text-sm text-muted">Saved.</span>}
161 <ErrorText>{actionData?.error}</ErrorText>
162 </div>
163 </Form>
164 </>
165 );
166}

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