g1t/services/deployments/src/moves.ts
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 quotas | 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 | ||
| 16 | export type MoveKind = "production" | "preview"; | |
| 17 | ||
| 18 | /** An app row, as far as a move needs it. */ | |
| 19 | export 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). */ | |
| 30 | export 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. */ | |
| 40 | export 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. */ | |
| 51 | export 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 | */ | |
| 60 | export 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. */ | |
| 83 | export 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 | */ | |
| 95 | export 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`. */ | |
| 102 | export 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 | */ | |
| 112 | export function ownerOf(app: { project_id: string; workspace: string }, owners: Map<string, string>): string { | |
| 113 | return owners.get(app.project_id) ?? app.workspace; | |
| 114 | } |