114 lines4,619 bytesCodeBlame
1/**
2 * Apps whose project moved: its workspace was renamed, or its repository
3 * renamed or transferred, so each app's name (`<project>-<workspace>`, or
4 * `<project>-git-<branch>-<workspace>`) is not the one it has now. Each is
5 * built again under its new name, from the commit it serves; once that is
6 * live, the old name redirects to it.
7 *
8 * What is stale is read from the rows as they are, not from the event that
9 * moved them, so a second delivery, a late one, or the sweep after a
10 * rebuild that could not start all end the same: the work left is found
11 * and done, and what is done is not done twice.
12 *
13 * Kept free of imports so its tests run on Node as they are.
14 */
15
16export type MoveKind = "production" | "preview";
17
18/** An app row, as far as a move needs it. */
19export type MovedApp = {
20 script: string;
21 project_id: string;
22 kind: MoveKind;
23 branch: string | null;
24 number: number | null;
25 commit_sha: string;
26 deployed_at: string;
27};
28
29/** A build a move dropped (it was under way under the old name). */
30export type DroppedBuild = {
31 project_id: string;
32 kind: MoveKind;
33 branch: string | null;
34 number: number | null;
35 commit_sha: string;
36 created_at: string;
37};
38
39/** One app to build again under its new name, and the old names it replaces. */
40export type MoveTarget = {
41 projectId: string;
42 kind: MoveKind;
43 branch: string | null;
44 number: number | null;
45 commit: string;
46 /** The app's scripts under older names, to redirect once the new one is live. */
47 from: string[];
48};
49
50/** Production, or one branch's preview, of a project: one app, whatever it is called. */
51export function appKey(row: { project_id: string; kind: MoveKind; branch: string | null }): string {
52 return `${row.project_id}/${row.kind}/${row.branch ?? ""}`;
53}
54
55/**
56 * What to build again: one target per app, from its stale rows and the
57 * builds a move dropped. The newest commit wins, so a build that was under
58 * way (newer than what was up) is the one built again.
59 */
60export function moveTargets(stale: MovedApp[], dropped: DroppedBuild[] = []): MoveTarget[] {
61 const targets = new Map<string, MoveTarget & { at: string }>();
62 const consider = (key: string, at: string, row: Omit<MoveTarget, "from">, script: string | null) => {
63 const found = targets.get(key);
64 if (!found) {
65 targets.set(key, { ...row, from: script ? [script] : [], at });
66 return;
67 }
68 if (script && !found.from.includes(script)) found.from.push(script);
69 if (at > found.at) Object.assign(found, { commit: row.commit, number: row.number ?? found.number, at });
70 };
71 for (const app of stale) {
72 const { project_id: projectId, kind, branch, number, commit_sha: commit } = app;
73 consider(appKey(app), app.deployed_at, { projectId, kind, branch, number, commit }, app.script);
74 }
75 for (const build of dropped) {
76 const { project_id: projectId, kind, branch, number, commit_sha: commit } = build;
77 consider(appKey(build), build.created_at, { projectId, kind, branch, number, commit }, null);
78 }
79 return [...targets.values()].map(({ at: _at, ...target }) => target);
80}
81
82/** What asking for a rebuild came to. */
83export type RebuildOutcome =
84 /** A build is queued or under way under the new name. */
85 | "queued"
86 /** It was asked for and refused, or failed to reach a sandbox: try again later. */
87 | "failed"
88 /** Nothing to build: deployments are off for it, or its pull request is closed. */
89 | "none";
90
91/**
92 * Reads what starting a rebuild returned: a deployment (queued, or one
93 * that was skipped or failed at once), nothing to build, or an error.
94 */
95export function rebuildOutcome(started: { ok: true; value: { status: string } } | { ok: false } | null): RebuildOutcome {
96 if (started == null) return "none";
97 if (!started.ok) return "failed";
98 return started.value.status === "queued" || started.value.status === "building" ? "queued" : "failed";
99}
100
101/** Whether a rebuild last tried at `lastTriedAt` may be tried again at `now`. */
102export function retryDue(lastTriedAt: string | null, now: number, afterMs: number): boolean {
103 return lastTriedAt == null || Date.parse(lastTriedAt) + afterMs <= now;
104}
105
106/**
107 * The workspace an app belongs to: its project's, as deployments has it
108 * now, falling back to the app row's own when the project is unknown. An
109 * app left under its old name after a move is the new workspace's; limits
110 * and plans are checked there, never against the workspace it moved from.
111 */
112export function ownerOf(app: { project_id: string; workspace: string }, owners: Map<string, string>): string {
113 return owners.get(app.project_id) ?? app.workspace;
114}