Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

required-checks.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

required-checks.tsxBlame269 lines · 1 contributor
a79a9edClaude1/**
2 * Block E6 — Required status checks matrix settings UI.
3 *
4 * GET /:owner/:repo/gates/protection/:id/checks — manage required checks
5 * POST /:owner/:repo/gates/protection/:id/checks — add a check name
6 * POST /:owner/:repo/gates/protection/:id/checks/:cid/delete — remove
7 *
8 * Required checks are scoped to a single branch-protection rule. Adding a
9 * check tells the merge handler "in addition to green gates, the check with
10 * this name must have a passing gate_run OR workflow_run against the head
11 * commit". Name matching is exact (case-sensitive); callers typically use
12 * workflow `name:` or the gate kinds (e.g. `GateTest`, `AI Review`).
13 */
14
15import { Hono } from "hono";
16import { and, eq } from "drizzle-orm";
17import { db } from "../db";
18import {
19 branchProtection,
20 branchRequiredChecks,
21 repositories,
22 users,
23} from "../db/schema";
24import { Layout } from "../views/layout";
25import { RepoHeader, RepoNav } from "../views/components";
26import { softAuth, requireAuth } from "../middleware/auth";
27import type { AuthEnv } from "../middleware/auth";
28import { listRequiredChecks } from "../lib/branch-protection";
29import { audit } from "../lib/notify";
30
31const required = new Hono<AuthEnv>();
32required.use("*", softAuth);
33
34async function loadRepo(ownerName: string, repoName: string) {
35 try {
36 const [row] = await db
37 .select({
38 id: repositories.id,
39 name: repositories.name,
40 ownerId: repositories.ownerId,
41 starCount: repositories.starCount,
42 forkCount: repositories.forkCount,
43 })
44 .from(repositories)
45 .innerJoin(users, eq(repositories.ownerId, users.id))
46 .where(and(eq(users.username, ownerName), eq(repositories.name, repoName)))
47 .limit(1);
48 return row || null;
49 } catch {
50 return null;
51 }
52}
53
54async function loadRule(repositoryId: string, ruleId: string) {
55 try {
56 const [rule] = await db
57 .select()
58 .from(branchProtection)
59 .where(
60 and(
61 eq(branchProtection.id, ruleId),
62 eq(branchProtection.repositoryId, repositoryId)
63 )
64 )
65 .limit(1);
66 return rule || null;
67 } catch {
68 return null;
69 }
70}
71
72required.get(
73 "/:owner/:repo/gates/protection/:id/checks",
74 requireAuth,
75 async (c) => {
76 const user = c.get("user")!;
77 const { owner, repo, id } = c.req.param();
78 const repoRow = await loadRepo(owner, repo);
79 if (!repoRow) return c.notFound();
80 if (repoRow.ownerId !== user.id) {
81 return c.redirect(`/${owner}/${repo}/gates`);
82 }
83 const rule = await loadRule(repoRow.id, id);
84 if (!rule) {
85 return c.redirect(
86 `/${owner}/${repo}/gates/settings?error=${encodeURIComponent("Rule not found")}`
87 );
88 }
89
90 const checks = await listRequiredChecks(rule.id);
91 const success = c.req.query("success");
92 const error = c.req.query("error");
93
94 return c.html(
95 <Layout title={`Required checks — ${rule.pattern}`} user={user}>
96 <RepoHeader
97 owner={owner}
98 repo={repo}
99 starCount={repoRow.starCount}
100 forkCount={repoRow.forkCount}
101 currentUser={user.username}
102 />
103 <RepoNav owner={owner} repo={repo} active="gates" />
104
105 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
106 <h3>
107 Required checks · <code>{rule.pattern}</code>
108 </h3>
109 <a href={`/${owner}/${repo}/gates/settings`} class="btn btn-sm">
110 Back to protection
111 </a>
112 </div>
113
114 <p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">
115 Merges into branches matching this rule require a passing run for
116 each named check. Names match against <code>gate_runs.gate_name</code>{" "}
117 (e.g. <code>GateTest</code>, <code>AI Review</code>,{" "}
118 <code>Secret Scan</code>, <code>Type Check</code>) or the{" "}
119 <code>name:</code> field of a workflow in{" "}
120 <code>.gluecron/workflows/*.yml</code>.
121 </p>
122
123 {success && <div class="auth-success">{decodeURIComponent(success)}</div>}
124 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
125
126 <div class="panel" style="margin-bottom:16px">
127 {checks.length === 0 ? (
128 <div class="panel-empty">No required checks configured.</div>
129 ) : (
130 checks.map((ch) => (
131 <div class="panel-item" style="justify-content:space-between">
132 <code
133 style="background:var(--bg-tertiary);padding:2px 8px;border-radius:3px"
134 >
135 {ch.checkName}
136 </code>
137 <form
01ba63dClaude138 method="post"
a79a9edClaude139 action={`/${owner}/${repo}/gates/protection/${rule.id}/checks/${ch.id}/delete`}
140 onsubmit="return confirm('Remove this required check?')"
141 >
142 <button type="submit" class="btn btn-sm btn-danger">
143 Remove
144 </button>
145 </form>
146 </div>
147 ))
148 )}
149 </div>
150
151 <form
01ba63dClaude152 method="post"
a79a9edClaude153 action={`/${owner}/${repo}/gates/protection/${rule.id}/checks`}
154 class="panel"
155 style="padding:16px"
156 >
157 <div class="form-group">
158 <label>Check name</label>
159 <input
160 type="text"
161 name="checkName"
162 required
163 placeholder="GateTest"
164 style="font-family:var(--font-mono)"
165 />
166 </div>
167 <button type="submit" class="btn btn-primary">
168 Add required check
169 </button>
170 </form>
171 </Layout>
172 );
173 }
174);
175
176required.post(
177 "/:owner/:repo/gates/protection/:id/checks",
178 requireAuth,
179 async (c) => {
180 const user = c.get("user")!;
181 const { owner, repo, id } = c.req.param();
182 const repoRow = await loadRepo(owner, repo);
183 if (!repoRow) return c.notFound();
184 if (repoRow.ownerId !== user.id) {
185 return c.redirect(`/${owner}/${repo}/gates`);
186 }
187 const rule = await loadRule(repoRow.id, id);
188 if (!rule) {
189 return c.redirect(
190 `/${owner}/${repo}/gates/settings?error=${encodeURIComponent("Rule not found")}`
191 );
192 }
193
194 const body = await c.req.parseBody();
195 const checkName = String(body.checkName || "").trim();
196 if (!checkName) {
197 return c.redirect(
198 `/${owner}/${repo}/gates/protection/${rule.id}/checks?error=${encodeURIComponent("Name required")}`
199 );
200 }
201
202 try {
203 await db
204 .insert(branchRequiredChecks)
205 .values({ branchProtectionId: rule.id, checkName });
206 } catch (err) {
207 // Likely a unique-index collision — treat as success.
208 console.error("[required-checks] insert:", err);
209 }
210
211 await audit({
212 userId: user.id,
213 repositoryId: repoRow.id,
214 action: "branch_required_checks.create",
215 targetId: rule.id,
216 metadata: { checkName, pattern: rule.pattern },
217 });
218
219 return c.redirect(
220 `/${owner}/${repo}/gates/protection/${rule.id}/checks?success=${encodeURIComponent("Check added")}`
221 );
222 }
223);
224
225required.post(
226 "/:owner/:repo/gates/protection/:id/checks/:cid/delete",
227 requireAuth,
228 async (c) => {
229 const user = c.get("user")!;
230 const { owner, repo, id, cid } = c.req.param();
231 const repoRow = await loadRepo(owner, repo);
232 if (!repoRow) return c.notFound();
233 if (repoRow.ownerId !== user.id) {
234 return c.redirect(`/${owner}/${repo}/gates`);
235 }
236 const rule = await loadRule(repoRow.id, id);
237 if (!rule) {
238 return c.redirect(
239 `/${owner}/${repo}/gates/settings?error=${encodeURIComponent("Rule not found")}`
240 );
241 }
242
243 try {
244 await db
245 .delete(branchRequiredChecks)
246 .where(
247 and(
248 eq(branchRequiredChecks.id, cid),
249 eq(branchRequiredChecks.branchProtectionId, rule.id)
250 )
251 );
252 } catch (err) {
253 console.error("[required-checks] delete:", err);
254 }
255
256 await audit({
257 userId: user.id,
258 repositoryId: repoRow.id,
259 action: "branch_required_checks.delete",
260 targetId: rule.id,
261 });
262
263 return c.redirect(
264 `/${owner}/${repo}/gates/protection/${rule.id}/checks?success=${encodeURIComponent("Check removed")}`
265 );
266 }
267);
268
269export default required;