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.tsxBlame270 lines · 2 contributors
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"
63c60ebcopilot-swe-agent[bot]164 aria-label="Check name"
a79a9edClaude165 style="font-family:var(--font-mono)"
166 />
167 </div>
168 <button type="submit" class="btn btn-primary">
169 Add required check
170 </button>
171 </form>
172 </Layout>
173 );
174 }
175);
176
177required.post(
178 "/:owner/:repo/gates/protection/:id/checks",
179 requireAuth,
180 async (c) => {
181 const user = c.get("user")!;
182 const { owner, repo, id } = c.req.param();
183 const repoRow = await loadRepo(owner, repo);
184 if (!repoRow) return c.notFound();
185 if (repoRow.ownerId !== user.id) {
186 return c.redirect(`/${owner}/${repo}/gates`);
187 }
188 const rule = await loadRule(repoRow.id, id);
189 if (!rule) {
190 return c.redirect(
191 `/${owner}/${repo}/gates/settings?error=${encodeURIComponent("Rule not found")}`
192 );
193 }
194
195 const body = await c.req.parseBody();
196 const checkName = String(body.checkName || "").trim();
197 if (!checkName) {
198 return c.redirect(
199 `/${owner}/${repo}/gates/protection/${rule.id}/checks?error=${encodeURIComponent("Name required")}`
200 );
201 }
202
203 try {
204 await db
205 .insert(branchRequiredChecks)
206 .values({ branchProtectionId: rule.id, checkName });
207 } catch (err) {
208 // Likely a unique-index collision — treat as success.
209 console.error("[required-checks] insert:", err);
210 }
211
212 await audit({
213 userId: user.id,
214 repositoryId: repoRow.id,
215 action: "branch_required_checks.create",
216 targetId: rule.id,
217 metadata: { checkName, pattern: rule.pattern },
218 });
219
220 return c.redirect(
221 `/${owner}/${repo}/gates/protection/${rule.id}/checks?success=${encodeURIComponent("Check added")}`
222 );
223 }
224);
225
226required.post(
227 "/:owner/:repo/gates/protection/:id/checks/:cid/delete",
228 requireAuth,
229 async (c) => {
230 const user = c.get("user")!;
231 const { owner, repo, id, cid } = c.req.param();
232 const repoRow = await loadRepo(owner, repo);
233 if (!repoRow) return c.notFound();
234 if (repoRow.ownerId !== user.id) {
235 return c.redirect(`/${owner}/${repo}/gates`);
236 }
237 const rule = await loadRule(repoRow.id, id);
238 if (!rule) {
239 return c.redirect(
240 `/${owner}/${repo}/gates/settings?error=${encodeURIComponent("Rule not found")}`
241 );
242 }
243
244 try {
245 await db
246 .delete(branchRequiredChecks)
247 .where(
248 and(
249 eq(branchRequiredChecks.id, cid),
250 eq(branchRequiredChecks.branchProtectionId, rule.id)
251 )
252 );
253 } catch (err) {
254 console.error("[required-checks] delete:", err);
255 }
256
257 await audit({
258 userId: user.id,
259 repositoryId: repoRow.id,
260 action: "branch_required_checks.delete",
261 targetId: rule.id,
262 });
263
264 return c.redirect(
265 `/${owner}/${repo}/gates/protection/${rule.id}/checks?success=${encodeURIComponent("Check removed")}`
266 );
267 }
268);
269
270export default required;