Advance M7 workflows and release operations
This commit is contained in:
98
tools/web/create-rc-manifest.mjs
Normal file
98
tools/web/create-rc-manifest.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const releaseRoot = path.join(repoRoot, "release");
|
||||
const sha256Bytes = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const sha256 = (file) => sha256Bytes(fs.readFileSync(file));
|
||||
const archiveEntry = (archive, entry) => execFileSync("tar", ["-xOf", archive, entry], { maxBuffer: 32 * 1024 * 1024 });
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/package.json"), "utf8"));
|
||||
const engineManifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/app/public/engine-manifest.json"), "utf8"));
|
||||
const binaryArchive = path.join(releaseRoot, "blender-web-offline.tar.gz");
|
||||
const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz");
|
||||
const sbom = path.join(repoRoot, "docs/web/sbom.spdx.json");
|
||||
const sumsPath = path.join(releaseRoot, "SHA256SUMS.txt");
|
||||
const packageJsonPath = path.join(repoRoot, "web/package.json");
|
||||
const packageLockPath = path.join(repoRoot, "web/package-lock.json");
|
||||
const engineManifestPath = path.join(repoRoot, "web/app/public/engine-manifest.json");
|
||||
const embeddedPackageJsonBytes = archiveEntry(sourceArchive, "web/package.json");
|
||||
const embeddedPackageLockBytes = archiveEntry(sourceArchive, "web/package-lock.json");
|
||||
const embeddedEngineManifestBytes = archiveEntry(binaryArchive, "blender-web-offline/app/engine-manifest.json");
|
||||
const embeddedMetadataBytes = archiveEntry(binaryArchive, "blender-web-offline/release-metadata.json");
|
||||
const embeddedSbomBytes = archiveEntry(binaryArchive, "blender-web-offline/sbom.spdx.json");
|
||||
const sourceManifest = JSON.parse(archiveEntry(sourceArchive, "SOURCE_MANIFEST.json").toString("utf8"));
|
||||
const embeddedMetadata = JSON.parse(embeddedMetadataBytes.toString("utf8"));
|
||||
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf8" }).trim();
|
||||
const worktreeDirty = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim().length > 0;
|
||||
|
||||
if (!/^\d+\.\d+\.\d+-rc\.\d+$/.test(packageJson.version)) throw new Error("package version is not an RC semver");
|
||||
if (engineManifest.releaseId !== `blender-wasm-${packageJson.version}`) throw new Error("engine releaseId does not match RC semver");
|
||||
if (embeddedMetadata.productVersion !== packageJson.version || embeddedMetadata.engineReleaseId !== engineManifest.releaseId) {
|
||||
throw new Error("binary release metadata does not match the frozen RC identity");
|
||||
}
|
||||
if (sourceManifest.version !== packageJson.version || sourceManifest.engineReleaseId !== engineManifest.releaseId) {
|
||||
throw new Error("source manifest does not match the frozen RC identity");
|
||||
}
|
||||
for (const [label, actual, expected] of [
|
||||
["source package.json", embeddedPackageJsonBytes, fs.readFileSync(packageJsonPath)],
|
||||
["source package-lock.json", embeddedPackageLockBytes, fs.readFileSync(packageLockPath)],
|
||||
["binary engine manifest", embeddedEngineManifestBytes, fs.readFileSync(engineManifestPath)],
|
||||
["binary SBOM", embeddedSbomBytes, fs.readFileSync(sbom)],
|
||||
]) {
|
||||
if (!actual.equals(expected)) throw new Error(`${label} does not match the frozen workspace bytes`);
|
||||
}
|
||||
const checksumEntries = fs.readFileSync(sumsPath, "utf8").trim().split("\n").map((line) => line.match(/^([a-f0-9]{64}) ([^/]+)$/));
|
||||
if (checksumEntries.some((entry) => entry === null) || checksumEntries.length !== 2) {
|
||||
throw new Error("SHA256SUMS.txt must contain exactly two canonical archive entries");
|
||||
}
|
||||
const checksums = new Map(checksumEntries.map((entry) => [entry[2], entry[1]]));
|
||||
for (const file of [binaryArchive, sourceArchive]) {
|
||||
if (checksums.get(path.basename(file)) !== sha256(file)) throw new Error(`SHA256SUMS.txt does not match ${path.basename(file)}`);
|
||||
}
|
||||
|
||||
const artifact = (file, relative) => ({
|
||||
path: relative,
|
||||
bytes: fs.statSync(file).size,
|
||||
sha256: sha256(file),
|
||||
});
|
||||
const operations = ["deploy", "upgrade", "rollback", "diagnostics", "rehearsal"]
|
||||
.map((name) => [name, path.join(releaseRoot, "operations-reports", `${name}.json`)]);
|
||||
for (const [name, file] of operations) {
|
||||
if (!fs.existsSync(file)) throw new Error(`RC operations report is missing: ${name}`);
|
||||
}
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
rcId: `web-blender-${packageJson.version}`,
|
||||
semver: packageJson.version,
|
||||
gitCommit: commit,
|
||||
sourceBinding: {
|
||||
mode: "corresponding-source-archive",
|
||||
worktreeDirty,
|
||||
note: "The source archive SHA-256 binds the candidate bytes independently of the base commit.",
|
||||
},
|
||||
engineReleaseId: engineManifest.releaseId,
|
||||
artifacts: {
|
||||
binaryArchive: artifact(binaryArchive, "release/blender-web-offline.tar.gz"),
|
||||
sourceArchive: artifact(sourceArchive, "release/blender-web-corresponding-source.tar.gz"),
|
||||
sbom: artifact(sbom, "docs/web/sbom.spdx.json"),
|
||||
},
|
||||
bindings: {
|
||||
packageJsonSha256: sha256(packageJsonPath),
|
||||
packageLockSha256: sha256(packageLockPath),
|
||||
ledgerSha256: sha256(path.join(repoRoot, "docs/status/parity-ledger.json")),
|
||||
engineManifestSha256: sha256(engineManifestPath),
|
||||
embeddedReleaseMetadataSha256: sha256Bytes(embeddedMetadataBytes),
|
||||
sha256SumsSha256: sha256(sumsPath),
|
||||
},
|
||||
operations: Object.fromEntries(operations.map(([name, file]) => [name, artifact(file, path.relative(repoRoot, file).replaceAll(path.sep, "/"))])),
|
||||
};
|
||||
const output = path.join(releaseRoot, "RC_MANIFEST.json");
|
||||
fs.writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
fs.writeFileSync(`${output}.sha256`, `${sha256(output)} ${path.basename(output)}\n`);
|
||||
process.stdout.write(`rc-manifest-ok id=${manifest.rcId} commit=${commit} binary=${manifest.artifacts.binaryArchive.sha256} source=${manifest.artifacts.sourceArchive.sha256} sbom=${manifest.artifacts.sbom.sha256}\n`);
|
||||
Reference in New Issue
Block a user