"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { onRefsUpdated, startWorkflowsInBackground } from "@/lib/ci";
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);
startWorkflowsInBackground(onRefsUpdated(repo, new Map(), new Map([[`refs/tags/${tag}`, createdTag]]), actorId));
}
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
}
async function handle(fn: () => Promise<{ id: string; tag: string }>): Promise<ReleaseResult> {
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");
}