Skip to content

g1t/apps/web/app/lib/deployments.ts

65 lines2,800 bytesCodeBlame
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 */
5import 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). */
8const 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 */
15export 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. */
25export 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. */
35const 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 */
44export 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}