Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

177 lines · 8.3 KBRaw History
1/** @jsxImportSource hono/jsx */
2import type { FC } from "hono/jsx";
3import type { Env } from "../env.js";
4import { computePause, killSwitches, suspendedList, type KillSwitches, type Suspension } from "../lib/killswitch.js";
5import { Form, Time } from "./ui.js";
6
7/**
8 * Kill switches in the web UI (lib/killswitch.ts): the banners people see while one is on, and
9 * the **Kill switches** page of site administration (its routes are in account.tsx).
10 */
11
12const By: FC<{ s: Suspension }> = ({ s }) => (
13 <>
14 {s.by ? <> by {s.by}</> : null} <Time at={s.at} />
15 </>
16);
17
18/** On the admin pages: hosted compute is paused, so nobody forgets it is. */
19export const ComputePausedBanner: FC<{ env: Env }> = async ({ env }) => {
20 const p = await computePause(env).catch(() => null);
21 if (!p) return <></>;
22 return (
23 <div class="flash error" role="status" data-kill="paused">
24 <b>Hosted compute is paused on this instance.</b> Hosted Actions jobs fail without starting and no agent sandbox can start; self-hosted runners still work. Paused
25 <By s={p} />
26 {p.reason ? <>: {p.reason}</> : null}. <a href="/settings/admin/kill-switches">Kill switches</a>
27 </div>
28 );
29};
30
31/** What ActionsBlockedBanner shows, for a page to start reading early. Never rejects. */
32export const actionsSwitches = (env: Env, repoId: string): Promise<KillSwitches | null> => killSwitches(env, repoId).catch(() => null);
33
34/** On a repository's Actions pages and settings: its Actions are suspended, or hosted compute is paused. */
35export const ActionsBlockedBanner: FC<{ env: Env; repoId: string; switches?: Promise<KillSwitches | null> }> = async ({ env, repoId, switches }) => {
36 // `switches`: read by the page alongside its own data (actionsSwitches), so the banner doesn't wait for it after them.
37 const k = await (switches ?? actionsSwitches(env, repoId));
38 if (!k) return <></>;
39 const s = k.repo ?? k.account;
40 if (s)
41 return (
42 <div class="flash error" role="status" data-kill="suspended">
43 <b>Actions are suspended {k.repo ? "for this repository" : <>for every repository of {k.account!.login}</>} by a site administrator</b>
44 {s.reason ? <>: {s.reason}</> : null}. No workflow runs start, queued jobs fail, and agent sandboxes can't start until a site administrator lifts the suspension. Suspended
45 <By s={s} />.
46 </div>
47 );
48 if (k.paused)
49 return (
50 <div class="flash error" role="status" data-kill="paused">
51 <b>Hosted runners are paused on this instance by a site administrator</b>
52 {k.paused.reason ? <>: {k.paused.reason}</> : null}. Jobs on hosted runners fail without starting and agent sandboxes can't start; jobs on self-hosted runners still run.
53 </div>
54 );
55 return <></>;
56};
57
58/** The body of Settings → Kill switches. */
59export const KillSwitchesPage: FC<{ env: Env; t: string }> = async ({ env, t }) => {
60 const [paused, list] = await Promise.all([computePause(env), suspendedList(env)]);
61 return (
62 <>
63 <h1>Kill switches</h1>
64 <p class="muted">
65 Stop Actions and agent sandboxes quickly: for one repository, for every repository of an account, or hosted compute on the whole instance. Turning one on stops what is running at once; turning it off only lets new work start. Nobody is exempt, site administrators included. Every change is in the{" "}
66 <a href="/settings/audit">audit log</a>.
67 </p>
68 <div class="box">
69 <div class="head">
70 <h2>Hosted compute</h2>
71 {paused ? <span class="badge red">paused</span> : <span class="badge green">running</span>}
72 </div>
73 <div class="body">
74 {paused ? (
75 <>
76 <p class="m0">
77 Paused
78 <By s={paused} />
79 {paused.reason ? <>: {paused.reason}</> : null}.
80 </p>
81 <Form action="/settings/admin/kill-switches/resume" t={t} class="mt">
82 <button class="btn primary">Resume hosted compute</button>
83 </Form>
84 </>
85 ) : (
86 <Form action="/settings/admin/kill-switches/pause" t={t} confirm="Pause hosted compute on the whole instance? Running hosted jobs and open sandboxes stop now.">
87 <div class="formrow">
88 <div class="field grow">
89 <label for="pause-reason">Reason</label>
90 <input id="pause-reason" name="reason" required maxlength={500} placeholder="e.g. abuse investigation, cost overrun" />
91 </div>
92 <button class="btn danger">Pause hosted compute</button>
93 </div>
94 <div class="hint">Hosted Actions jobs fail without starting and no agent sandbox can start, in every repository. Self-hosted runners keep working.</div>
95 </Form>
96 )}
97 </div>
98 </div>
99 <Form action="/settings/admin/kill-switches/accounts" t={t} class="box" confirm="Suspend Actions for every repository of this account? Its runs are cancelled and its sandboxes closed now.">
100 <div class="head">
101 <h2>Suspend Actions for an account</h2>
102 </div>
103 <div class="body">
104 <div class="formrow">
105 <div class="field">
106 <label for="ks-login">Person or organization</label>
107 <input id="ks-login" name="login" required placeholder="login" autocomplete="off" />
108 </div>
109 <div class="field grow">
110 <label for="ks-areason">Reason</label>
111 <input id="ks-areason" name="reason" required maxlength={500} />
112 </div>
113 <button class="btn danger">Suspend</button>
114 </div>
115 <div class="hint">Every repository the account owns: no workflow runs (hosted or self-hosted) and no agent sandboxes.</div>
116 </div>
117 </Form>
118 <Form action="/settings/admin/kill-switches/repos" t={t} class="box" confirm="Suspend Actions for this repository? Its runs are cancelled and its sandboxes closed now.">
119 <div class="head">
120 <h2>Suspend Actions for a repository</h2>
121 </div>
122 <div class="body">
123 <div class="formrow">
124 <div class="field">
125 <label for="ks-repo">Repository</label>
126 <input id="ks-repo" name="repo" required placeholder="owner/name" autocomplete="off" />
127 </div>
128 <div class="field grow">
129 <label for="ks-rreason">Reason</label>
130 <input id="ks-rreason" name="reason" required maxlength={500} />
131 </div>
132 <button class="btn danger">Suspend</button>
133 </div>
134 </div>
135 </Form>
136 <div class="box">
137 <div class="head">
138 <h2>Suspended</h2>
139 <span class="badge">{list.accounts.length + list.repos.length}</span>
140 </div>
141 {list.accounts.length + list.repos.length ? (
142 <ul class="list">
143 {list.accounts.map((a) => (
144 <li>
145 <div class="grow">
146 <b>{a.login}</b> <span class="badge">{a.kind === "org" ? "organization" : "person"}</span> <span class="muted small">every repository · suspended<By s={a} /></span>
147 {a.reason ? <div class="meta">{a.reason}</div> : null}
148 </div>
149 <Form action={`/settings/admin/kill-switches/accounts/${encodeURIComponent(a.login)}/lift`} t={t} class="inline">
150 <button class="btn sm">Lift</button>
151 </Form>
152 </li>
153 ))}
154 {list.repos.map((r) => (
155 <li>
156 <div class="grow">
157 <a href={`/${r.owner}/${r.name}/actions`}>
158 <b>
159 {r.owner}/{r.name}
160 </b>
161 </a>{" "}
162 <span class="badge">repository</span> <span class="muted small">suspended<By s={r} /></span>
163 {r.reason ? <div class="meta">{r.reason}</div> : null}
164 </div>
165 <Form action={`/settings/admin/kill-switches/repos/${encodeURIComponent(r.owner)}/${encodeURIComponent(r.name)}/lift`} t={t} class="inline">
166 <button class="btn sm">Lift</button>
167 </Form>
168 </li>
169 ))}
170 </ul>
171 ) : (
172 <div class="empty">No account or repository has its Actions suspended.</div>
173 )}
174 </div>
175 </>
176 );
177};