| 1786 | 1786 | | ]); |
| 1787 | 1787 | | } |
| 1788 | 1788 | | |
| 1789 | + | private async takeDownBranchPreview(projectId: string, branch: string): Promise<void> { |
| 1790 | + | const apps = await this.db |
| 1791 | + | .prepare("SELECT script FROM apps WHERE project_id = ? AND kind = 'preview' AND number IS NULL AND branch = ?") |
| 1792 | + | .bind(projectId, branch) |
| 1793 | + | .all<{ script: string }>(); |
| 1794 | + | for (const app of apps.results) await this.removeApp(app.script); |
| 1795 | + | } |
| 1796 | + | |
| 1789 | 1797 | | private async takeDownWhere(projectId: string, kind: DeployKind | null, branch?: string): Promise<void> { |
| 1790 | 1798 | | const apps = await this.db |
| 1791 | 1799 | | .prepare( |
| ⋯ |
| 1856 | 1864 | | for (const app of apps.results) await this.removeApp(app.script); |
| 1857 | 1865 | | } |
| 1858 | 1866 | | break; |
| 1867 | + | // A branch deleted takes its own preview (not a pull request's) down. |
| 1868 | + | case "git.deleted": |
| 1869 | + | if (event.data.ref.startsWith("refs/heads/")) { |
| 1870 | + | const branch = event.data.ref.slice("refs/heads/".length); |
| 1871 | + | for (const project of await this.projects.byRepo(event.data.repoId)) await this.takeDownBranchPreview(project.id, branch); |
| 1872 | + | } |
| 1873 | + | break; |
| 1859 | 1874 | | case "git.push": |
| 1860 | 1875 | | if (!event.data.defaultBranch) break; |
| 1861 | 1876 | | for (const project of await this.projects.byRepo(event.data.repoId)) { |
| ⋯ |
| 2156 | 2171 | | for (const [id, project] of await this.projectsById(unknown)) projects.set(id, project); |
| 2157 | 2172 | | const billing = billingClient(this.env.BILLING); |
| 2158 | 2173 | | const open = new Map<string, boolean>(); |
| 2159 | | − | const actors = new Map<string, User | null>(); |
| 2160 | 2174 | | |
| 2161 | 2175 | | for (const target of targets) { |
| 2162 | 2176 | | const row = settings.get(target.projectId)!; |
| ⋯ |
| 2204 | 2218 | | result.waiting++; |
| 2205 | 2219 | | continue; |
| 2206 | 2220 | | } |
| 2207 | | − | if (!actors.has(project.workspace)) actors.set(project.workspace, await this.workspaceActor(project.workspace)); |
| 2208 | 2221 | | const started = |
| 2209 | 2222 | | target.kind === "production" |
| 2210 | 2223 | | ? await this.deployProduction(project, target.commit, "g1t") |
| 2211 | 2224 | | : target.number != null |
| 2212 | 2225 | | ? await this.deployPreview(project, target.number, "g1t", true) |
| 2213 | | − | : await this.rebuildStack(project, target.branch, target.commit, actors.get(project.workspace) ?? null); |
| 2226 | + | : // A preview no pull request holds is not built again; the sweep takes it down. |
| 2227 | + | null; |
| 2214 | 2228 | | const outcome = rebuildOutcome(started); |
| 2215 | 2229 | | if (outcome === "queued") result.queued++; |
| 2216 | 2230 | | else if (outcome === "failed") { |
| ⋯ |
| 2466 | 2480 | | return { ...project, workspace: current, source }; |
| 2467 | 2481 | | } |
| 2468 | 2482 | | |
| 2469 | | − | /** A stack's preview (no pull request of its own) built again at `commit`. */ |
| 2470 | | − | private async rebuildStack(project: Project, branch: string | null, commit: string, actor: User | null): Promise<Result<Deployment> | null> { |
| 2471 | | − | if (!actor || branch == null) return null; |
| 2472 | | − | const settings = await this.settingsRow(project.id); |
| 2473 | | − | if (!settings?.enabled || !settings.previews || settings.repo_deleted_at) return null; |
| 2474 | | − | return this.start({ |
| 2475 | | − | project, |
| 2476 | | − | kind: "preview", |
| 2477 | | − | branch, |
| 2478 | | − | number: null, |
| 2479 | | − | commit, |
| 2480 | | − | source: repoOf(project).path, |
| 2481 | | − | reader: actor, |
| 2482 | | − | createdBy: "g1t", |
| 2483 | | − | settings, |
| 2484 | | − | // As `stack` built it: the project's own default branch. |
| 2485 | | − | trusted: true, |
| 2486 | | − | }); |
| 2487 | | − | } |
| 2488 | | − | |
| 2489 | 2483 | | // ---- The sweep ----------------------------------------------------- |
| 2490 | 2484 | | |
| 2491 | 2485 | | /** |
| ⋯ |
| 2509 | 2503 | | .all<{ id: string }>(); |
| 2510 | 2504 | | for (const { id } of stuck.results) await this.finishFailed(id, "The build did not finish in 45 minutes.", null, null); |
| 2511 | 2505 | | }); |
| 2506 | + | await step("taking down previews no pull request holds", () => this.takeDownUnheld()); |
| 2512 | 2507 | | await step("taking down idle previews", () => this.takeDownIdle()); |
| 2513 | 2508 | | await step("charging months", () => this.chargeMonths()); |
| 2514 | 2509 | | |
| ⋯ |
| 2726 | 2721 | | } |
| 2727 | 2722 | | } |
| 2728 | 2723 | | |
| 2724 | + | /** |
| 2725 | + | * Previews of a branch that is gone. A pull request's preview comes down |
| 2726 | + | * when it closes or merges; a branch's preview (built by a project's |
| 2727 | + | * deploy workflow, or by the preview stacks projects' dependencies once |
| 2728 | + | * built) has no pull request, so it comes down once its branch is |
| 2729 | + | * deleted, here for any whose deletion was missed. A deleted |
| 2730 | + | * workspace's stay paused for a restore. |
| 2731 | + | */ |
| 2732 | + | private async takeDownUnheld(): Promise<void> { |
| 2733 | + | const unheld = await this.db |
| 2734 | + | .prepare( |
| 2735 | + | `SELECT apps.script, apps.project_id, apps.branch FROM apps JOIN settings ON settings.project_id = apps.project_id |
| 2736 | + | WHERE apps.kind = 'preview' AND apps.number IS NULL AND settings.workspace_deleted_at IS NULL`, |
| 2737 | + | ) |
| 2738 | + | .all<{ script: string; project_id: string; branch: string | null }>(); |
| 2739 | + | if (unheld.results.length === 0) return; |
| 2740 | + | const projects = await this.projectsById([...new Set(unheld.results.map((app) => app.project_id))]); |
| 2741 | + | const branchesOf = new Map<string, Set<string> | null>(); |
| 2742 | + | for (const app of unheld.results) { |
| 2743 | + | const project = projects.get(app.project_id); |
| 2744 | + | if (!project || project.source.kind !== "hosted") continue; |
| 2745 | + | if (!branchesOf.has(project.id)) { |
| 2746 | + | const actor = await this.workspaceActor(project.workspace); |
| 2747 | + | const branches = actor ? await reposClient(this.env.REPOS).branches(repoOf(project).path, actor) : null; |
| 2748 | + | // A read that failed says nothing: the app stays until one succeeds. |
| 2749 | + | branchesOf.set(project.id, branches?.ok ? new Set(branches.value.map((b) => b.name)) : null); |
| 2750 | + | } |
| 2751 | + | const names = branchesOf.get(project.id); |
| 2752 | + | if (names && (app.branch == null || !names.has(app.branch))) await this.removeApp(app.script); |
| 2753 | + | } |
| 2754 | + | } |
| 2755 | + | |
| 2729 | 2756 | | /** Previews no one has visited in their project's idle days. */ |
| 2730 | 2757 | | private async takeDownIdle(): Promise<void> { |
| 2731 | 2758 | | const idle = await this.db |