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.tsBlame149 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// ---------------------------------------------------------------------------
59statuses.post(
60 "/api/v1/repos/:owner/:repo/statuses/:sha",
61 softAuth,
62 requireAuth,
63 async (c) => {
64 const { owner: ownerName, repo: repoName, sha } = c.req.param();
65 const user = c.get("user")!;
66 if (!isValidSha(sha)) {
67 return c.json({ error: "Invalid sha" }, 400);
68 }
69 const resolved = await resolveRepo(ownerName, repoName);
70 if (!resolved) return c.json({ error: "Repository not found" }, 404);
71 if (resolved.owner.id !== user.id) {
72 return c.json({ error: "Forbidden" }, 403);
73 }
74
75 let body: any = {};
76 try {
77 body = await c.req.json();
78 } catch {
79 body = {};
80 }
81
82 const state = body.state;
83 if (!isValidState(state)) {
84 return c.json(
85 {
86 error:
87 "Invalid state; must be one of pending, success, failure, error",
88 },
89 400
90 );
91 }
92
93 const row = await setStatus({
94 repositoryId: resolved.repo.id,
95 commitSha: sha,
96 state,
97 context: body.context ?? body.Context ?? "default",
98 description: body.description ?? null,
99 targetUrl: body.target_url ?? body.targetUrl ?? null,
100 creatorId: user.id,
101 });
102
103 if (!row) return c.json({ error: "Could not save status" }, 500);
104
105 return c.json({ ok: true, status: row });
106 }
107);
108
109// ---------------------------------------------------------------------------
110// GET statuses list
111// ---------------------------------------------------------------------------
112statuses.get(
113 "/api/v1/repos/:owner/:repo/commits/:sha/statuses",
114 softAuth,
115 async (c) => {
116 const { owner: ownerName, repo: repoName, sha } = c.req.param();
117 if (!isValidSha(sha)) return c.json({ error: "Invalid sha" }, 400);
118 const resolved = await resolveRepo(ownerName, repoName);
119 if (!resolved) return c.json({ error: "Repository not found" }, 404);
120 const user = c.get("user");
121 if (resolved.repo.isPrivate && (!user || user.id !== resolved.owner.id)) {
122 return c.json({ error: "Forbidden" }, 403);
123 }
124 const rows = await listStatuses(resolved.repo.id, sha);
125 return c.json({ total: rows.length, statuses: rows });
126 }
127);
128
129// ---------------------------------------------------------------------------
130// GET combined status
131// ---------------------------------------------------------------------------
132statuses.get(
133 "/api/v1/repos/:owner/:repo/commits/:sha/status",
134 softAuth,
135 async (c) => {
136 const { owner: ownerName, repo: repoName, sha } = c.req.param();
137 if (!isValidSha(sha)) return c.json({ error: "Invalid sha" }, 400);
138 const resolved = await resolveRepo(ownerName, repoName);
139 if (!resolved) return c.json({ error: "Repository not found" }, 404);
140 const user = c.get("user");
141 if (resolved.repo.isPrivate && (!user || user.id !== resolved.owner.id)) {
142 return c.json({ error: "Forbidden" }, 403);
143 }
144 const combined = await combinedStatus(resolved.repo.id, sha);
145 return c.json(combined);
146 }
147);
148
149export default statuses;