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

commit-statuses.ts

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

commit-statuses.tsBlame158 lines · 1 contributor
0cdfd89Claude1/**
2 * Block J8 — Commit status API (GitHub-parity).
3 *
4 * External CI / automation systems POST statuses against a (repo, sha, context)
5 * triple. Reads are public for public repos (softAuth visibility check) and
6 * writes require repo-owner auth (session / OAuth / PAT accepted by
7 * requireAuth).
8 *
9 * POST /api/v1/repos/:owner/:repo/statuses/:sha
10 * body: { state, context?, description?, target_url? }
11 * 200: { ok: true, status }
12 * 400: invalid state / sha
13 * 401/403: auth / permission
14 *
15 * GET /api/v1/repos/:owner/:repo/commits/:sha/statuses
16 * 200: { total, statuses: [...] }
17 *
18 * GET /api/v1/repos/:owner/:repo/commits/:sha/status
19 * 200: { state, total, counts, contexts }
20 */
21
22import { Hono } from "hono";
23import { and, eq } from "drizzle-orm";
24import { db } from "../db";
25import { repositories, users } from "../db/schema";
26import { softAuth, requireAuth } from "../middleware/auth";
27import type { AuthEnv } from "../middleware/auth";
28import {
29 combinedStatus,
30 isValidSha,
31 isValidState,
32 listStatuses,
33 setStatus,
34} from "../lib/commit-statuses";
35
36const statuses = new Hono<AuthEnv>();
37
38async function resolveRepo(ownerName: string, repoName: string) {
39 const [owner] = await db
40 .select()
41 .from(users)
42 .where(eq(users.username, ownerName))
43 .limit(1);
44 if (!owner) return null;
45 const [repo] = await db
46 .select()
47 .from(repositories)
48 .where(
49 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
50 )
51 .limit(1);
52 if (!repo) return null;
53 return { owner, repo };
54}
55
56// ---------------------------------------------------------------------------
57// POST status
58// ---------------------------------------------------------------------------
052c2e6Claude59/**
60 * Handler body for POST <prefix>/repos/:owner/:repo/statuses/:sha — shared
61 * between the v1 mount here (session/OAuth/PAT via softAuth+requireAuth) and
62 * the v2 alias in `src/routes/api-v2.ts` (PAT via apiAuth+requireApiAuth +
63 * `repo` scope). The behaviour is identical; the only difference is which
64 * middleware stack authenticates the user.
65 */
66export async function postCommitStatusHandler(c: any) {
67 const { owner: ownerName, repo: repoName, sha } = c.req.param();
68 const user = c.get("user")!;
69 if (!isValidSha(sha)) {
70 return c.json({ error: "Invalid sha" }, 400);
71 }
72 const resolved = await resolveRepo(ownerName, repoName);
73 if (!resolved) return c.json({ error: "Repository not found" }, 404);
74 if (resolved.owner.id !== user.id) {
75 return c.json({ error: "Forbidden" }, 403);
76 }
0cdfd89Claude77
052c2e6Claude78 let body: any = {};
79 try {
80 body = await c.req.json();
81 } catch {
82 body = {};
83 }
0cdfd89Claude84
052c2e6Claude85 const state = body.state;
86 if (!isValidState(state)) {
87 return c.json(
88 {
89 error:
90 "Invalid state; must be one of pending, success, failure, error",
91 },
92 400
93 );
94 }
0cdfd89Claude95
052c2e6Claude96 const row = await setStatus({
97 repositoryId: resolved.repo.id,
98 commitSha: sha,
99 state,
100 context: body.context ?? body.Context ?? "default",
101 description: body.description ?? null,
102 targetUrl: body.target_url ?? body.targetUrl ?? null,
103 creatorId: user.id,
104 });
0cdfd89Claude105
052c2e6Claude106 if (!row) return c.json({ error: "Could not save status" }, 500);
0cdfd89Claude107
052c2e6Claude108 return c.json({ ok: true, status: row });
109}
110
111statuses.post(
112 "/api/v1/repos/:owner/:repo/statuses/:sha",
113 softAuth,
114 requireAuth,
115 postCommitStatusHandler
0cdfd89Claude116);
117
118// ---------------------------------------------------------------------------
119// GET statuses list
120// ---------------------------------------------------------------------------
121statuses.get(
122 "/api/v1/repos/:owner/:repo/commits/:sha/statuses",
123 softAuth,
124 async (c) => {
125 const { owner: ownerName, repo: repoName, sha } = c.req.param();
126 if (!isValidSha(sha)) return c.json({ error: "Invalid sha" }, 400);
127 const resolved = await resolveRepo(ownerName, repoName);
128 if (!resolved) return c.json({ error: "Repository not found" }, 404);
129 const user = c.get("user");
130 if (resolved.repo.isPrivate && (!user || user.id !== resolved.owner.id)) {
131 return c.json({ error: "Forbidden" }, 403);
132 }
133 const rows = await listStatuses(resolved.repo.id, sha);
134 return c.json({ total: rows.length, statuses: rows });
135 }
136);
137
138// ---------------------------------------------------------------------------
139// GET combined status
140// ---------------------------------------------------------------------------
141statuses.get(
142 "/api/v1/repos/:owner/:repo/commits/:sha/status",
143 softAuth,
144 async (c) => {
145 const { owner: ownerName, repo: repoName, sha } = c.req.param();
146 if (!isValidSha(sha)) return c.json({ error: "Invalid sha" }, 400);
147 const resolved = await resolveRepo(ownerName, repoName);
148 if (!resolved) return c.json({ error: "Repository not found" }, 404);
149 const user = c.get("user");
150 if (resolved.repo.isPrivate && (!user || user.id !== resolved.owner.id)) {
151 return c.json({ error: "Forbidden" }, 403);
152 }
153 const combined = await combinedStatus(resolved.repo.id, sha);
154 return c.json(combined);
155 }
156);
157
158export default statuses;