feat: add fail-closed signed release provenance

This commit is contained in:
NuklearRabbit
2026-07-29 16:30:02 +02:00
parent 8e580a0b13
commit 4708367b66
6 changed files with 93 additions and 6 deletions
+32 -1
View File
@@ -116,13 +116,20 @@ app.whenReady().then(async () => {
target_commitish: commit,
name: `ForgeFlow ${version}`,
body,
draft: false,
draft: true,
prerelease: false,
}),
},
);
}
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 }),
});
}
const binaries = [
path.join(root, "dist", `ForgeFlow-Setup-${version}-win-x64.exe`),
path.join(root, "dist", `ForgeFlow-Portable-${version}-win-x64.exe`),
@@ -171,6 +178,30 @@ app.whenReady().then(async () => {
console.log(`PASS published ${name}`);
}
}
for (const [name, type] of [
[`ForgeFlow-${version}-provenance.json`, "application/json"],
[`ForgeFlow-${version}-sbom.cdx.json`, "application/vnd.cyclonedx+json"],
]) {
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 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 requiredAssets = [
...binaries.flatMap((binaryPath) => [path.basename(binaryPath), `${path.basename(binaryPath)}.sha256`]),
`ForgeFlow-${version}-provenance.json`,
`ForgeFlow-${version}-sbom.cdx.json`,
];
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)}`,
);