import { readdir } from "node:fs/promises"; import { basename, join } from "node:path"; interface Release { id: number; tag_name: string; upload_url?: string; } const root = process.cwd(); const packageJson = JSON.parse(await Bun.file(join(root, "package.json")).text()) as { version: string }; const version = packageJson.version; const tag = `v${version}`; const repository = process.env.GITEA_REPOSITORY; const token = process.env.GITEA_TOKEN; const apiUrl = (process.env.GITEA_API_URL ?? "").replace(/\/$/, ""); const releaseDir = join(root, "release"); const releaseName = `gitea-github-theme-v${version}`; if (!repository) { throw new Error("GITEA_REPOSITORY is required"); } if (!token) { throw new Error("GITEA_TOKEN is required"); } if (!apiUrl) { throw new Error("GITEA_API_URL is required"); } const [owner, repo] = repository.split("/"); const headers = { Accept: "application/json", Authorization: `token ${token}`, }; function resolveApiUrl(pathOrUrl: string) { if (/^https?:\/\//.test(pathOrUrl)) { return pathOrUrl; } return `${apiUrl}${pathOrUrl}`; } async function request(pathOrUrl: string, init: RequestInit = {}) { const url = resolveApiUrl(pathOrUrl); const response = await fetch(url, { ...init, headers: { ...headers, ...(init.headers ?? {}), }, }); if (response.status === 404) { return null; } if (!response.ok) { throw new Error(`${init.method ?? "GET"} ${url} failed: ${response.status} ${await response.text()}`); } if (response.status === 204) { return null; } return response.json(); } function releaseAssetUploadUrl(release: Release, artifact: string) { if (!release.upload_url) { return `/repos/${owner}/${repo}/releases/${release.id}/assets?name=${encodeURIComponent(artifact)}`; } const baseUrl = release.upload_url.replace(/\{.*\}$/, ""); const separator = baseUrl.includes("?") ? "&" : "?"; return `${baseUrl}${separator}name=${encodeURIComponent(artifact)}`; } async function ignoreMissing(path: string, init: RequestInit = {}) { try { await request(path, init); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!message.includes("404")) { throw error; } } } const currentRelease = (await request(`/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`)) as Release | null; if (currentRelease) { await ignoreMissing(`/repos/${owner}/${repo}/releases/${currentRelease.id}`, { method: "DELETE" }); } await ignoreMissing(`/repos/${owner}/${repo}/tags/${encodeURIComponent(tag)}`, { method: "DELETE" }); const release = (await request(`/repos/${owner}/${repo}/releases`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tag_name: tag, target_commitish: process.env.GITEA_SHA, name: releaseName, body: [ `Gitea GitHub theme build for ${tag}.`, "", "Install by extracting the archive and copying `custom/` into your Gitea custom path.", ].join("\n"), draft: false, prerelease: false, }), })) as Release; const artifacts = (await readdir(releaseDir)).filter((file) => file.endsWith(".zip") || file.endsWith(".tar.gz") || file === "checksums.txt"); for (const artifact of artifacts) { const path = join(releaseDir, artifact); const file = Bun.file(path); await request(releaseAssetUploadUrl(release, basename(path)), { method: "POST", headers: { "Content-Type": "application/octet-stream", }, body: file, }); } console.log(`published release ${tag} with ${artifacts.length} assets`);