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.
| 0cdfd89 | 1 | /** |
| 2 | * Block J8 — Commit statuses (GitHub-parity external CI signal). | |
| 3 | * | |
| 4 | * External systems POST per-commit (sha, context) statuses that appear on | |
| 5 | * the commit detail view and combined-status rollup endpoints. Upsert | |
| 6 | * semantics: a post with the same (repo, sha, context) replaces the prior | |
| 7 | * row. State vocabulary: pending | success | failure | error. | |
| 8 | */ | |
| 9 | ||
| 10 | import { and, desc, eq } from "drizzle-orm"; | |
| 11 | import { db } from "../db"; | |
| 12 | import { commitStatuses, type CommitStatus } from "../db/schema"; | |
| 13 | ||
| 14 | export type StatusState = "pending" | "success" | "failure" | "error"; | |
| 15 | ||
| 16 | export const STATUS_STATES: StatusState[] = [ | |
| 17 | "pending", | |
| 18 | "success", | |
| 19 | "failure", | |
| 20 | "error", | |
| 21 | ]; | |
| 22 | ||
| 23 | const CONTEXT_MAX = 120; | |
| 24 | const DESCRIPTION_MAX = 1000; | |
| 25 | const URL_MAX = 2048; | |
| 26 | ||
| 27 | export interface SetStatusInput { | |
| 28 | repositoryId: string; | |
| 29 | commitSha: string; | |
| 30 | state: StatusState; | |
| 31 | context?: string | null; | |
| 32 | description?: string | null; | |
| 33 | targetUrl?: string | null; | |
| 34 | creatorId?: string | null; | |
| 35 | } | |
| 36 | ||
| 37 | /** Git short-sha / full-sha sanity check. */ | |
| 38 | export function isValidSha(sha: string | null | undefined): boolean { | |
| 39 | if (!sha) return false; | |
| 40 | if (sha.length < 4 || sha.length > 40) return false; | |
| 41 | return /^[a-f0-9]+$/i.test(sha); | |
| 42 | } | |
| 43 | ||
| 44 | export function isValidState(s: unknown): s is StatusState { | |
| 45 | return typeof s === "string" && STATUS_STATES.includes(s as StatusState); | |
| 46 | } | |
| 47 | ||
| 48 | export function sanitiseContext(ctx: string | null | undefined): string { | |
| 49 | const raw = (ctx || "default").trim(); | |
| 50 | if (!raw) return "default"; | |
| 51 | return raw.slice(0, CONTEXT_MAX); | |
| 52 | } | |
| 53 | ||
| 54 | function clamp( | |
| 55 | s: string | null | undefined, | |
| 56 | max: number | |
| 57 | ): string | null { | |
| 58 | if (!s) return null; | |
| 59 | const t = s.toString(); | |
| 60 | if (!t.length) return null; | |
| 61 | return t.slice(0, max); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Upsert a commit status. Returns the final row. Throws only on bad state | |
| 66 | * input — callers normalise via `isValidState` first. | |
| 67 | */ | |
| 68 | export async function setStatus( | |
| 69 | input: SetStatusInput | |
| 70 | ): Promise<CommitStatus | null> { | |
| 71 | if (!isValidState(input.state)) return null; | |
| 72 | if (!isValidSha(input.commitSha)) return null; | |
| 73 | const ctx = sanitiseContext(input.context); | |
| 74 | const sha = input.commitSha.toLowerCase(); | |
| 75 | const description = clamp(input.description, DESCRIPTION_MAX); | |
| 76 | const targetUrl = clamp(input.targetUrl, URL_MAX); | |
| 77 | // Delete-then-insert keeps the table simple without relying on ON CONFLICT. | |
| 78 | await db | |
| 79 | .delete(commitStatuses) | |
| 80 | .where( | |
| 81 | and( | |
| 82 | eq(commitStatuses.repositoryId, input.repositoryId), | |
| 83 | eq(commitStatuses.commitSha, sha), | |
| 84 | eq(commitStatuses.context, ctx) | |
| 85 | ) | |
| 86 | ); | |
| 87 | const [row] = await db | |
| 88 | .insert(commitStatuses) | |
| 89 | .values({ | |
| 90 | repositoryId: input.repositoryId, | |
| 91 | commitSha: sha, | |
| 92 | state: input.state, | |
| 93 | context: ctx, | |
| 94 | description, | |
| 95 | targetUrl, | |
| 96 | creatorId: input.creatorId || null, | |
| 97 | }) | |
| 98 | .returning(); | |
| 99 | return row || null; | |
| 100 | } | |
| 101 | ||
| 102 | /** List statuses for a commit, newest first. */ | |
| 103 | export async function listStatuses( | |
| 104 | repositoryId: string, | |
| 105 | commitSha: string | |
| 106 | ): Promise<CommitStatus[]> { | |
| 107 | if (!isValidSha(commitSha)) return []; | |
| 108 | return db | |
| 109 | .select() | |
| 110 | .from(commitStatuses) | |
| 111 | .where( | |
| 112 | and( | |
| 113 | eq(commitStatuses.repositoryId, repositoryId), | |
| 114 | eq(commitStatuses.commitSha, commitSha.toLowerCase()) | |
| 115 | ) | |
| 116 | ) | |
| 117 | .orderBy(desc(commitStatuses.updatedAt)); | |
| 118 | } | |
| 119 | ||
| 120 | export interface CombinedStatus { | |
| 121 | state: StatusState | "success"; | |
| 122 | total: number; | |
| 123 | counts: Record<StatusState, number>; | |
| 124 | contexts: Array<{ | |
| 125 | context: string; | |
| 126 | state: StatusState; | |
| 127 | description: string | null; | |
| 128 | targetUrl: string | null; | |
| 129 | updatedAt: Date; | |
| 130 | }>; | |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Reduce a list of statuses to a single roll-up state. | |
| 135 | * any failure/error → "failure" | |
| 136 | * any pending → "pending" | |
| 137 | * all success → "success" | |
| 138 | * empty list → "success" (no signal means no blocker) | |
| 139 | * | |
| 140 | * Pure — exposed for tests. | |
| 141 | */ | |
| 142 | export function reduceCombined(states: StatusState[]): StatusState { | |
| 143 | if (!states.length) return "success" as StatusState; | |
| 144 | if (states.some((s) => s === "failure" || s === "error")) return "failure"; | |
| 145 | if (states.some((s) => s === "pending")) return "pending"; | |
| 146 | return "success"; | |
| 147 | } | |
| 148 | ||
| 149 | /** | |
| 150 | * GitHub-style combined status. Groups statuses by context (latest per | |
| 151 | * context wins — which the upsert guarantees anyway) and reduces to a | |
| 152 | * single state. | |
| 153 | */ | |
| 154 | export async function combinedStatus( | |
| 155 | repositoryId: string, | |
| 156 | commitSha: string | |
| 157 | ): Promise<CombinedStatus> { | |
| 158 | const rows = await listStatuses(repositoryId, commitSha); | |
| 159 | const byContext = new Map<string, CommitStatus>(); | |
| 160 | for (const r of rows) { | |
| 161 | const prev = byContext.get(r.context); | |
| 162 | if (!prev || prev.updatedAt < r.updatedAt) byContext.set(r.context, r); | |
| 163 | } | |
| 164 | const latest = [...byContext.values()]; | |
| 165 | const counts: Record<StatusState, number> = { | |
| 166 | pending: 0, | |
| 167 | success: 0, | |
| 168 | failure: 0, | |
| 169 | error: 0, | |
| 170 | }; | |
| 171 | for (const r of latest) { | |
| 172 | if (isValidState(r.state)) counts[r.state]++; | |
| 173 | } | |
| 174 | const state = reduceCombined(latest.map((r) => r.state as StatusState)); | |
| 175 | return { | |
| 176 | state, | |
| 177 | total: latest.length, | |
| 178 | counts, | |
| 179 | contexts: latest | |
| 180 | .sort((a, b) => a.context.localeCompare(b.context)) | |
| 181 | .map((r) => ({ | |
| 182 | context: r.context, | |
| 183 | state: r.state as StatusState, | |
| 184 | description: r.description, | |
| 185 | targetUrl: r.targetUrl, | |
| 186 | updatedAt: r.updatedAt, | |
| 187 | })), | |
| 188 | }; | |
| 189 | } | |
| 190 | ||
| 191 | export const __internal = { | |
| 192 | CONTEXT_MAX, | |
| 193 | DESCRIPTION_MAX, | |
| 194 | URL_MAX, | |
| 195 | clamp, | |
| 196 | }; |