Allow authenticated Gitea Actions release publishing

This commit is contained in:
2026-08-30 00:39:26 +02:00
parent d8a4f357fb
commit ca428128d0
+76 -18
View File
@@ -20,6 +20,15 @@ function safeRepositoryPart(value, label) {
return text; 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 = {}) { async function api(baseUrl, token, pathname, options = {}) {
const response = await fetch(`${baseUrl}/api/v1${pathname}`, { const response = await fetch(`${baseUrl}/api/v1${pathname}`, {
...options, ...options,
@@ -51,26 +60,39 @@ app.whenReady().then(async () => {
await fs.readFile(path.join(root, "package.json"), "utf8"), await fs.readFile(path.join(root, "package.json"), "utf8"),
); );
const configPath = path.join(configuredUserData, "forgeflow-config.json"); const configPath = path.join(configuredUserData, "forgeflow-config.json");
const config = JSON.parse(await fs.readFile(configPath, "utf8")); 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) { if (!config?.gitea?.encryptedToken) {
throw new Error( throw new Error(
`No encrypted Gitea token was found in ${configPath}. Sign in to Gitea once from ForgeFlow first.`, `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.`,
); );
} }
const token = safeStorage.decryptString( token = safeStorage.decryptString(
Buffer.from(config.gitea.encryptedToken, "base64"), 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( const owner = safeRepositoryPart(
process.env.FORGEFLOW_RELEASE_OWNER || config.updates?.owner || "Jens", process.env.FORGEFLOW_RELEASE_OWNER || config?.updates?.owner || "Jens",
"Release repository owner", "Release repository owner",
); );
const repo = safeRepositoryPart( const repo = safeRepositoryPart(
process.env.FORGEFLOW_RELEASE_REPO || config.updates?.repo || "ForgeFlow", process.env.FORGEFLOW_RELEASE_REPO || config?.updates?.repo || "ForgeFlow",
"Release repository name", "Release repository name",
); );
const branch = safeRepositoryPart( const branch = safeRepositoryPart(
process.env.FORGEFLOW_RELEASE_BRANCH || config.updates?.branch || "main", process.env.FORGEFLOW_RELEASE_BRANCH || config?.updates?.branch || "main",
"Release branch", "Release branch",
); );
const version = manifest.version; const version = manifest.version;
@@ -122,11 +144,16 @@ app.whenReady().then(async () => {
} }
if (release.draft !== true) { if (release.draft !== true) {
release = await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, { release = await api(
baseUrl,
token,
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`,
{
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ draft: true }), body: JSON.stringify({ draft: true }),
}); },
);
} }
const binaries = [ const binaries = [
path.join(root, "dist", `ForgeFlow-Setup-${version}-win-x64.exe`), 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"], [`ForgeFlow-${version}-release-manifest.json.sig`, "application/octet-stream"],
]) { ]) {
const bytes = await fs.readFile(path.join(root, "dist", name)); const bytes = await fs.readFile(path.join(root, "dist", name));
const existing = (release.assets || []).find((asset) => asset.name === name); const existing = (release.assets || []).find(
if (existing) await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets/${existing.id}`, { method: "DELETE" }); (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(); const form = new FormData();
form.append("attachment", new Blob([bytes], { type }), name); 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 }); const uploaded = await api(
release.assets = [...(release.assets || []).filter((asset) => asset.name !== name), uploaded]; 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 = [ 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}-provenance.json`,
`ForgeFlow-${version}-sbom.cdx.json`, `ForgeFlow-${version}-sbom.cdx.json`,
`ForgeFlow-${version}-release-manifest.json`, `ForgeFlow-${version}-release-manifest.json`,
`ForgeFlow-${version}-release-manifest.json.sig`, `ForgeFlow-${version}-release-manifest.json.sig`,
]; ];
const missingAssets = requiredAssets.filter((name) => !(release.assets || []).some((asset) => asset.name === name)); const missingAssets = requiredAssets.filter(
if (missingAssets.length) throw new Error(`Release remains draft because required assets are missing: ${missingAssets.join(", ")}`); (name) => !(release.assets || []).some((asset) => asset.name === name),
release = await api(baseUrl, token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}`, { );
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", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ draft: false }), body: JSON.stringify({ draft: false }),
}); },
);
console.log( console.log(
`PASS ForgeFlow ${version} binary release published to ${owner}/${repo} for ${commit.slice(0, 7)}`, `PASS ForgeFlow ${version} binary release published to ${owner}/${repo} for ${commit.slice(0, 7)}`,
); );