"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { person } from "@/lib/actor"; import { afterRefUpdated } from "@/lib/push"; import { GitError } from "@/lib/git"; import { createRelease, deleteAsset, deleteRelease, findReleaseById, publishRelease, ReleaseError, updateRelease, type ReleaseInput, } from "@/lib/releases"; import { markPushed, type RepoRecord } from "@/lib/repos"; import { ActionError, repoForAction } from "./guard"; export type ReleaseResult = { error?: string; release?: { id: string; tag: string } }; async function releaseOrThrow(repo: RepoRecord, id: string) { const release = await findReleaseById(repo.id, id); if (!release) throw new ActionError("Release not found."); return release; } /** A tag created from the browser starts workflows just like a push of it would. */ async function afterSave(repo: RepoRecord, tag: string, createdTag: string | null, actorId: string) { if (createdTag) { await markPushed(repo.id); afterRefUpdated(repo, `refs/tags/${tag}`, null, createdTag, person(actorId)); } revalidatePath(`/${repo.owner}/${repo.name}`, "layout"); } async function handle(fn: () => Promise<{ id: string; tag: string }>): Promise { try { return { release: await fn() }; } catch (e) { if (e instanceof ActionError || e instanceof ReleaseError || e instanceof GitError) return { error: e.message }; throw e; } } /** * Creates a release (`releaseId` null) or saves one. The browser uploads the files afterwards, so * when it has files to add it saves a new release as a draft and publishes it once they're in. */ export async function saveReleaseAction(owner: string, name: string, releaseId: string | null, form: ReleaseInput) { return handle(async () => { const { repo, viewer } = await repoForAction(owner, name, "write"); const input: ReleaseInput = { tag: String(form.tag ?? "").trim(), target: String(form.target ?? "").trim(), title: String(form.title ?? ""), body: String(form.body ?? ""), prerelease: form.prerelease === true, draft: form.draft === true, }; const { release, createdTag } = releaseId ? await updateRelease(repo, await releaseOrThrow(repo, releaseId), input) : await createRelease(repo, viewer.id, input); await afterSave(repo, release.tag, createdTag, viewer.id); return { id: release.id, tag: release.tag }; }); } export async function publishReleaseAction(owner: string, name: string, releaseId: string) { return handle(async () => { const { repo, viewer } = await repoForAction(owner, name, "write"); const { release, createdTag } = await publishRelease(repo, await releaseOrThrow(repo, releaseId)); await afterSave(repo, release.tag, createdTag, viewer.id); return { id: release.id, tag: release.tag }; }); } export async function deleteReleaseAction(owner: string, name: string, releaseId: string) { const { repo } = await repoForAction(owner, name, "write"); await deleteRelease(await releaseOrThrow(repo, releaseId)); revalidatePath(`/${repo.owner}/${repo.name}`, "layout"); redirect(`/${repo.owner}/${repo.name}/releases`); } export async function deleteReleaseAssetAction(owner: string, name: string, releaseId: string, assetId: string) { const { repo } = await repoForAction(owner, name, "write"); const release = await releaseOrThrow(repo, releaseId); const asset = release.assets.find((a) => a.id === assetId); if (asset) await deleteAsset(release, asset); revalidatePath(`/${repo.owner}/${repo.name}`, "layout"); }