Skip to content
192 linesCodeBlameRaw
1import { Form, Link } from "react-router";
2
3import { ACTIONS_ACCESS_LEVELS, APPROVAL_POLICIES } from "@g1t/contracts";
4import type { ActionsAccessLevel, 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 access = String(form.get("accessLevel"));
41 if ((ACTIONS_ACCESS_LEVELS as readonly string[]).includes(access)) change.accessLevel = access as ActionsAccessLevel;
42 const saved = await actions.setActionsSettings(user, { namespace: params.owner, name: params.repo }, change);
43 return saved.ok ? { saved: true, error: null } : { saved: false, error: saved.error.message };
44}
45
46/** Each approval policy, least strict first, and whose runs it holds. */
47const POLICY_WORDS: Record<ApprovalPolicy, { label: string; about: string }> = {
48 first_time_contributors: {
49 label: "First-time contributors",
50 about: "Someone outside the workspace who has not had a pull request merged here.",
51 },
52 outside_contributors: {
53 label: "Outside contributors",
54 about: "Those, and everyone outside the workspace who cannot push here: pull requests from forks, and from people with Read or Triage.",
55 },
56 all_external_contributors: {
57 label: "All external contributors",
58 about: "Everyone outside the workspace, outside collaborators with Write included.",
59 },
60};
61
62export default function RepoActionsSettings({ loaderData, actionData, params }: Route.ComponentProps) {
63 const base = `/${params.owner}/${params.repo}`;
64 const { settings } = loaderData;
65 return (
66 <>
67 <RepoSettingsHeading base={base} />
68 <Form method="post" className="max-w-4xl space-y-8">
69 <Section
70 title="Workflow permissions"
71 about={
72 <>
73 What the token of a job without <code className="font-mono text-xs">permissions:</code> can do. Workflows and
74 jobs that write <code className="font-mono text-xs">permissions:</code> get what they write.
75 </>
76 }
77 >
78 <RadioGroup
79 name="defaultPermissions"
80 defaultValue={settings.defaultChosen ? settings.defaultPermissions : "inherit"}
81 className="gap-3"
82 >
83 <RadioOption
84 value="inherit"
85 label="As the workspace says"
86 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.`}
87 />
88 <RadioOption
89 value="read"
90 label="Read repository contents and packages"
91 description={
92 <>
93 <code className="font-mono">contents: read</code> and <code className="font-mono">packages: read</code>.
94 </>
95 }
96 />
97 <RadioOption
98 value="write"
99 label="Read and write"
100 disabled={settings.maxPermissions === "read"}
101 description={
102 settings.maxPermissions === "read"
103 ? "The workspace holds its repositories to read-only."
104 : "Read and write to everything a job's token can reach in this repository."
105 }
106 />
107 </RadioGroup>
108 <p className="text-sm text-muted">
109 Whatever a workflow asks for, a pull request from outside the repository's writers gets a token that can only
110 read.
111 </p>
112 <input type="hidden" name="pullRequestsShown" value={settings.workspaceAllowsPullRequests ? "1" : ""} disabled={!settings.workspaceAllowsPullRequests} />
113 <CheckboxOption
114 name="canApprovePullRequests"
115 defaultChecked={settings.canApprovePullRequests}
116 disabled={!settings.workspaceAllowsPullRequests}
117 label="Allow g1t Actions to create and approve pull requests"
118 description={
119 settings.workspaceAllowsPullRequests
120 ? "Jobs' tokens may open pull requests and approve them. Off unless you turn it on."
121 : "The workspace does not allow it: an owner can, in the workspace's Actions settings."
122 }
123 />
124 </Section>
125
126 <Section
127 title="Approval for pull requests from outside"
128 about={
129 <>
130 Whose pull requests' runs wait as <span className="text-fg/85">Approval required</span> until someone with the
131 Write role approves them. Nothing runs before then, and no token or secret is handed out.
132 </>
133 }
134 >
135 <RadioGroup name="approvalPolicy" defaultValue={settings.approvalPolicy} className="gap-3">
136 {APPROVAL_POLICIES.map((policy) => (
137 <RadioOption
138 key={policy}
139 value={policy}
140 label={
141 <>
142 {POLICY_WORDS[policy].label}
143 {policy === "outside_contributors" && <span className="text-muted">(the default)</span>}
144 </>
145 }
146 description={POLICY_WORDS[policy].about}
147 />
148 ))}
149 </RadioGroup>
150 <p className="text-sm text-muted">
151 Members' pull requests never wait, nor does g1t's own work. Each new push to a pull request that waits, waits
152 again. See{" "}
153 <Link to={`${base}/settings/environments`} className="text-fg underline-offset-2 hover:underline">
154 Environments
155 </Link>{" "}
156 to make deployments wait for a review.
157 </p>
158 </Section>
159
160 <Section
161 title="Access"
162 about={
163 <>
164 Which other repositories' workflows may use this repository's actions (
165 <code className="font-mono text-xs">uses: {params.owner}/{params.repo}@main</code>) and reusable workflows while it
166 is private. A public repository's actions and workflows are anyone's.
167 </>
168 }
169 >
170 <RadioGroup name="accessLevel" defaultValue={settings.accessLevel ?? "none"} className="gap-3">
171 <RadioOption
172 value="none"
173 label="Not accessible"
174 description="Only this repository's own workflows use them. The default."
175 />
176 <RadioOption
177 value="organization"
178 label={`Accessible from repositories in ${params.owner}`}
179 description={`Workflows in ${params.owner}'s other private repositories may use them. A public repository's workflows never can, since their logs are public.`}
180 />
181 </RadioGroup>
182 </Section>
183
184 <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">
185 <SubmitButton pending="Saving…">Save settings</SubmitButton>
186 {actionData?.saved && <span className="text-sm text-muted">Saved.</span>}
187 <ErrorText>{actionData?.error}</ErrorText>
188 </div>
189 </Form>
190 </>
191 );
192}