Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1 | /** |
| 2 | * Builds as the deployments pages show them: why one failed, in plain | |
| 3 | * words, and a run of the same failure as one row. | |
| 4 | */ | |
| 5 | import type { Deployment } from "@g1t/contracts"; | |
| 6 | ||
| 7 | /** What git says when the commit asked for is not in the repository (as services/deployments/src/retries.ts). */ | |
| 8 | const MISSING_COMMIT = /reference is not a tree|not our ref|bad object|unknown revision|no such commit/i; | |
| 9 | ||
| 10 | /** | |
| 11 | * Why a build failed or was skipped, as the page says it. A commit that is | |
| 12 | * gone (force-pushed over, or its branch deleted) is said plainly; older | |
| 13 | * builds still carry git's own words, which stay in the build log. | |
| 14 | */ | |
| 15 | export function buildError(build: Pick<Deployment, "kind" | "number" | "error">): string | null { | |
| 16 | if (!build.error || !MISSING_COMMIT.test(build.error)) return build.error; | |
| 17 | if (build.kind === "preview" && build.number != null) { | |
| 18 | return `This pull request's commit no longer exists. Push again, or close pull request #${build.number}.`; | |
| 19 | } | |
| 20 | if (build.kind === "preview") return "This branch's commit no longer exists. Push to the branch again."; | |
| 21 | return "This commit no longer exists in the repository. Push to the default branch again."; | |
| 22 | } | |
| 23 | ||
| 24 | /** A build, and how many times in a row before it its app failed the same way. */ | |
| 25 | export type BuildGroup<T> = { | |
| 26 | /** The newest of them, the one the row links to. */ | |
| 27 | build: T; | |
| 28 | /** 1 for a build on its own. */ | |
| 29 | count: number; | |
| 30 | /** When the oldest of them started. */ | |
| 31 | firstAt: string; | |
| 32 | }; | |
| 33 | ||
| 34 | /** The app a build is for: production, or one branch's preview. */ | |
| 35 | const appOf = (build: Pick<Deployment, "kind" | "branch">) => `${build.kind}/${build.branch ?? ""}`; | |
| 36 | ||
| 37 | /** | |
| 38 | * Folds each run of identical failures of one app (production, or one | |
| 39 | * branch's preview), newest first as listed, into the row of its newest: | |
| 40 | * the same error, with no other outcome for that app in between. Builds | |
| 41 | * of other apps between them do not break the run; any other build of | |
| 42 | * the same app does. | |
| 43 | */ | |
| 44 | export function groupBuilds<T extends Pick<Deployment, "kind" | "branch" | "number" | "status" | "error" | "createdAt">>( | |
| 45 | builds: T[], | |
| 46 | ): BuildGroup<T>[] { | |
| 47 | const groups: BuildGroup<T>[] = []; | |
| 48 | // Each app's run still open, by the error it failed with. | |
| 49 | const open = new Map<string, { group: BuildGroup<T>; error: string | null }>(); | |
| 50 | for (const build of builds) { | |
| 51 | const app = appOf(build); | |
| 52 | const run = open.get(app); | |
| 53 | const error = buildError(build); | |
| 54 | if (build.status === "failed" && run && run.error === error) { | |
| 55 | run.group.count++; | |
| 56 | run.group.firstAt = build.createdAt; | |
| 57 | continue; | |
| 58 | } | |
| 59 | const group = { build, count: 1, firstAt: build.createdAt }; | |
| 60 | groups.push(group); | |
| 61 | if (build.status === "failed") open.set(app, { group, error }); | |
| 62 | else open.delete(app); | |
| 63 | } | |
| 64 | return groups; | |
| 65 | } |