| 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 | } |