g1t/services/deployments/src/moves.ts

114 lines4,619 bytesCodeBlame

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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
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}