diff --git a/scripts/publish-binary-release.cjs b/scripts/publish-binary-release.cjs index b8cc6b2..e6d3f2f 100644 --- a/scripts/publish-binary-release.cjs +++ b/scripts/publish-binary-release.cjs @@ -20,6 +20,15 @@ function safeRepositoryPart(value, label) { return text; } +async function readOptionalConfig(configPath) { + try { + return JSON.parse(await fs.readFile(configPath, "utf8")); + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } +} + async function api(baseUrl, token, pathname, options = {}) { const response = await fetch(`${baseUrl}/api/v1${pathname}`, { ...options, @@ -51,26 +60,39 @@ app.whenReady().then(async () => { await fs.readFile(path.join(root, "package.json"), "utf8"), ); const configPath = path.join(configuredUserData, "forgeflow-config.json"); - const config = JSON.parse(await fs.readFile(configPath, "utf8")); - if (!config?.gitea?.encryptedToken) { - throw new Error( - `No encrypted Gitea token was found in ${configPath}. Sign in to Gitea once from ForgeFlow first.`, + const config = await readOptionalConfig(configPath); + const actionsToken = String( + process.env.GITEA_TOKEN || process.env.FORGEFLOW_RELEASE_TOKEN || "", + ).trim(); + let token = actionsToken; + if (!token) { + if (!config?.gitea?.encryptedToken) { + throw new Error( + `No release token was supplied and no encrypted Gitea token was found in ${configPath}. Sign in to Gitea once from ForgeFlow or run from Gitea Actions with GITEA_TOKEN.`, + ); + } + token = safeStorage.decryptString( + Buffer.from(config.gitea.encryptedToken, "base64"), ); } - const token = safeStorage.decryptString( - Buffer.from(config.gitea.encryptedToken, "base64"), - ); - const baseUrl = normalizeBaseUrl(config.gitea.baseUrl); + const configuredBaseUrl = + process.env.FORGEFLOW_RELEASE_BASE_URL || config?.gitea?.baseUrl; + if (!configuredBaseUrl) { + throw new Error( + "No Gitea release base URL was supplied. Set FORGEFLOW_RELEASE_BASE_URL or configure Gitea in ForgeFlow.", + ); + } + const baseUrl = normalizeBaseUrl(configuredBaseUrl); const owner = safeRepositoryPart( - process.env.FORGEFLOW_RELEASE_OWNER || config.updates?.owner || "Jens", + process.env.FORGEFLOW_RELEASE_OWNER || config?.updates?.owner || "Jens", "Release repository owner", ); const repo = safeRepositoryPart( - process.env.FORGEFLOW_RELEASE_REPO || config.updates?.repo || "ForgeFlow", + process.env.FORGEFLOW_RELEASE_REPO || config?.updates?.repo || "ForgeFlow", "Release repository name", ); const branch = safeRepositoryPart( - process.env.FORGEFLOW_RELEASE_BRANCH || config.updates?.branch || "main", + process.env.FORGEFLOW_RELEASE_BRANCH || config?.updates?.branch || "main", "Release branch", ); const version = manifest.version; @@ -122,11 +144,16 @@ app.whenReady().then(async () => { } if (release.draft !== true) { - release = await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ draft: true }), - }); + release = await api( + baseUrl, + token, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ draft: true }), + }, + ); } const binaries = [ path.join(root, "dist", `ForgeFlow-Setup-${version}-win-x64.exe`), @@ -183,27 +210,58 @@ app.whenReady().then(async () => { [`ForgeFlow-${version}-release-manifest.json.sig`, "application/octet-stream"], ]) { const bytes = await fs.readFile(path.join(root, "dist", name)); - const existing = (release.assets || []).find((asset) => asset.name === name); - if (existing) await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets/${existing.id}`, { method: "DELETE" }); + const existing = (release.assets || []).find( + (asset) => asset.name === name, + ); + if (existing) { + await api( + baseUrl, + token, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets/${existing.id}`, + { method: "DELETE" }, + ); + } const form = new FormData(); form.append("attachment", new Blob([bytes], { type }), name); - const uploaded = await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, { method: "POST", body: form, timeout: 300_000 }); - release.assets = [...(release.assets || []).filter((asset) => asset.name !== name), uploaded]; + const uploaded = await api( + baseUrl, + token, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, + { method: "POST", body: form, timeout: 300_000 }, + ); + release.assets = [ + ...(release.assets || []).filter((asset) => asset.name !== name), + uploaded, + ]; } const requiredAssets = [ - ...binaries.flatMap((binaryPath) => [path.basename(binaryPath), `${path.basename(binaryPath)}.sha256`]), + ...binaries.flatMap((binaryPath) => [ + path.basename(binaryPath), + `${path.basename(binaryPath)}.sha256`, + ]), `ForgeFlow-${version}-provenance.json`, `ForgeFlow-${version}-sbom.cdx.json`, `ForgeFlow-${version}-release-manifest.json`, `ForgeFlow-${version}-release-manifest.json.sig`, ]; - const missingAssets = requiredAssets.filter((name) => !(release.assets || []).some((asset) => asset.name === name)); - if (missingAssets.length) throw new Error(`Release remains draft because required assets are missing: ${missingAssets.join(", ")}`); - release = await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ draft: false }), - }); + const missingAssets = requiredAssets.filter( + (name) => !(release.assets || []).some((asset) => asset.name === name), + ); + if (missingAssets.length) { + throw new Error( + `Release remains draft because required assets are missing: ${missingAssets.join(", ")}`, + ); + } + release = await api( + baseUrl, + token, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ draft: false }), + }, + ); console.log( `PASS ForgeFlow ${version} binary release published to ${owner}/${repo} for ${commit.slice(0, 7)}`, );