Allow authenticated Gitea Actions release publishing
This commit is contained in:
@@ -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)}`,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user