import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { first } from "~/server/db"; import { badRequest, notFoundError } from "~/server/errors"; import { limitWrite } from "~/server/limits"; import { assertNotArchived, requireRepoAccess } from "~/server/repos"; import { branchSha, listRefs } from "~/server/git/refs"; import { pagesDomain, siteUrl } from "~/server/pages/config"; import { customDomainsEnabled } from "~/server/pages/domains"; import { createDeployment, loadSite, removePreview, rollbackTo } from "~/server/pages/deploy"; import { disableSite, listDeployments, refreshDomain, saveSite, setDomain } from "~/server/pages/site"; import { repoRef } from "~/lib/validation"; import type { Viewer } from "~/lib/types"; const refName = z.string().min(1).max(255); async function adminSite(owner: string, name: string, viewer: Viewer | null, write = false) { const { repo } = await requireRepoAccess(owner, name, requireViewer(viewer), "admin"); if (write) assertNotArchived(repo); const site = await loadSite(repo.id); return { repo, site }; } export const getPages = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer); const refs = await listRefs(repo); return { enabled: pagesDomain() !== null, public: repo.visibility === "public", customDomainsEnabled: customDomainsEnabled(), defaultBranch: repo.default_branch, branches: refs.branches.slice(0, 200).map((branch) => branch.name), url: siteUrl(repo.owner_name, repo.name, null), site: site ? { source: site.source, branch: site.branch, folder: site.folder, artifactName: site.artifact_name, spa: site.spa === 1, previews: site.previews === 1, customDomain: site.custom_domain, domainStatus: site.domain_status, activeDeploymentId: site.active_deployment_id, } : null, deployments: site ? await listDeployments(repo) : [], }; }); export const savePages = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ source: z.enum(["branch", "workflow"]), branch: refName, folder: z.string().max(200), artifactName: z.string().regex(/^[A-Za-z0-9._-]{1,100}$/), spa: z.boolean(), previews: z.boolean(), }), ) .handler(async ({ data, context }) => { const { repo } = await adminSite(data.owner, data.repo, context.viewer, true); await limitWrite(requireViewer(context.viewer)); if (!pagesDomain()) throw badRequest("Pages hosting is not configured on this forge"); await saveSite(repo, data); return { ok: true }; }); export const disablePages = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer); await limitWrite(requireViewer(context.viewer)); if (site) await disableSite(repo, site); return { ok: true }; }); export const deployPages = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const { repo, site } = await adminSite(data.owner, data.repo, viewer, true); if (!site) throw badRequest("Enable Pages first"); await limitWrite(viewer); const ref = `refs/heads/${site.branch}`; if (site.source === "branch") { const sha = branchSha(await listRefs(repo), site.branch); if (!sha) throw badRequest(`Branch ${site.branch} does not exist`); const row = await createDeployment({ repo, site, environment: "production", prNumber: null, ref, sha, artifactId: null, creatorId: viewer.id, }); return { id: row.id }; } const artifact = await first<{ id: string; sha: string }>( `SELECT a.id, r.sha FROM job_artifacts a JOIN workflow_runs r ON r.id = a.run_id WHERE r.repo_id = ? AND r.ref = ? AND r.conclusion = 'success' AND r.event != 'pull_request' AND a.name = ? ORDER BY r.created_at DESC LIMIT 1`, repo.id, ref, site.artifact_name, ); if (!artifact) throw badRequest(`No successful run on ${site.branch} uploaded an artifact named "${site.artifact_name}"`); const row = await createDeployment({ repo, site, environment: "production", prNumber: null, ref, sha: artifact.sha, artifactId: artifact.id, creatorId: viewer.id, }); return { id: row.id }; }); export const rollbackPages = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ deploymentId: z.string().min(1) })) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer, true); if (!site) throw notFoundError("Pages is not enabled"); await limitWrite(requireViewer(context.viewer)); await rollbackTo(repo, site, data.deploymentId); return { ok: true }; }); export const removePagesPreview = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ prNumber: z.number().int().positive() })) .handler(async ({ data, context }) => { const { repo } = await adminSite(data.owner, data.repo, context.viewer); await limitWrite(requireViewer(context.viewer)); await removePreview(repo, data.prNumber); return { ok: true }; }); export const setPagesDomain = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ domain: z.string().max(253).nullable() })) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer, true); if (!site) throw badRequest("Enable Pages first"); await limitWrite(requireViewer(context.viewer)); await setDomain(repo, site, data.domain); return { ok: true }; }); export const checkPagesDomain = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer, true); if (!site) throw badRequest("Enable Pages first"); await limitWrite(requireViewer(context.viewer)); return refreshDomain(repo, site); });