Files
workinf_Blender_Wasm/tools/web/check-upgrade-runbook.mjs
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

118 lines
6.5 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
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 archive = path.join(repoRoot, "release/blender-web-offline.tar.gz");
const reportRoot = path.join(repoRoot, "release/operations-reports");
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-upgrade-runbook-"));
const sha256Bytes = (value) => crypto.createHash("sha256").update(value).digest("hex");
const sha256File = (file) => sha256Bytes(fs.readFileSync(file));
function readMetadata(release) {
return JSON.parse(fs.readFileSync(path.join(release, "release-metadata.json"), "utf8"));
}
function switchCurrent(installRoot, relativeTarget) {
const pending = path.join(installRoot, ".current.next");
fs.rmSync(pending, { force: true });
fs.symlinkSync(relativeTarget, pending);
fs.renameSync(pending, path.join(installRoot, "current"));
}
try {
const installRoot = path.join(workspace, "install");
const releases = path.join(installRoot, "releases");
const originStorage = path.join(workspace, "origin-storage");
fs.mkdirSync(releases, { recursive: true });
fs.mkdirSync(originStorage);
const extracted = path.join(workspace, "extracted");
fs.mkdirSync(extracted);
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", extracted]);
const newRelease = path.join(releases, sha256File(archive));
fs.renameSync(path.join(extracted, "blender-web-offline"), newRelease);
const newMetadata = readMetadata(newRelease);
const oldRelease = path.join(releases, "synthetic-previous-release");
fs.cpSync(newRelease, oldRelease, { recursive: true });
const oldMetadata = structuredClone(newMetadata);
oldMetadata.productVersion = "0.0.9";
oldMetadata.engineReleaseId = `${newMetadata.engineReleaseId}-previous`;
oldMetadata.storage.indexedDbSchemaVersion = Math.max(1, newMetadata.storage.indexedDbSchemaVersion - 1);
const oldWorker = oldMetadata.app.workerAssets[0];
const previousWorker = oldWorker.replace(/\.worker-([A-Za-z0-9_-]+)\.js$/, ".worker-previous.js");
assert.notEqual(previousWorker, oldWorker, "Worker asset does not have a content-hashed name");
fs.copyFileSync(path.join(oldRelease, "app", oldWorker), path.join(oldRelease, "app", previousWorker));
oldMetadata.app.workerAssets = [previousWorker, ...oldMetadata.app.workerAssets.slice(1)];
fs.writeFileSync(path.join(oldRelease, "release-metadata.json"), `${JSON.stringify(oldMetadata, null, 2)}\n`);
switchCurrent(installRoot, path.join("releases", path.basename(oldRelease)));
const projectBytes = Buffer.from("M6-17B persistent OPFS project bytes");
const projectFile = path.join(originStorage, "scene.blend");
fs.writeFileSync(projectFile, projectBytes);
const projectBefore = sha256File(projectFile);
assert.notEqual(oldMetadata.engineReleaseId, newMetadata.engineReleaseId, "upgrade does not change engine release identity");
assert.ok(newMetadata.storage.indexedDbSchemaVersion >= oldMetadata.storage.indexedDbSchemaVersion, "upgrade would downgrade IndexedDB schema");
assert.equal(newMetadata.storage.opfsProjectManifestSchemaVersion, oldMetadata.storage.opfsProjectManifestSchemaVersion, "upgrade changes OPFS schema without a migration");
assert.notDeepEqual(newMetadata.app.workerAssets, oldMetadata.app.workerAssets, "upgrade does not exercise Worker identity switching");
assert.equal(newMetadata.app.documentCache, "no-cache");
assert.equal(newMetadata.app.manifestCache, "no-cache");
assert.equal(newMetadata.app.hashedAssetCache, "public, max-age=31536000, immutable");
const oldTarget = fs.realpathSync(path.join(installRoot, "current"));
const failedPending = path.join(installRoot, ".current.next");
fs.symlinkSync(path.join("releases", "missing-release"), failedPending);
assert.throws(() => fs.realpathSync(failedPending), /ENOENT/);
fs.rmSync(failedPending);
assert.equal(fs.realpathSync(path.join(installRoot, "current")), oldTarget, "failed preflight changed current");
assert.equal(sha256File(projectFile), projectBefore, "failed preflight changed project data");
switchCurrent(installRoot, path.join("releases", path.basename(newRelease)));
assert.equal(fs.realpathSync(path.join(installRoot, "current")), newRelease);
assert.deepEqual(readMetadata(path.join(installRoot, "current")), newMetadata);
assert.equal(sha256File(projectFile), projectBefore, "release switch changed origin storage");
for (const worker of newMetadata.app.workerAssets) {
assert.ok(fs.statSync(path.join(newRelease, "app", worker)).isFile(), `new Worker is missing: ${worker}`);
}
assert.ok(fs.statSync(path.join(oldRelease, "app", previousWorker)).isFile(), "old Worker was removed inside the rollback window");
const report = {
schemaVersion: 1,
task: "M6-17B",
status: "READY",
old: {
productVersion: oldMetadata.productVersion,
engineReleaseId: oldMetadata.engineReleaseId,
indexedDbSchemaVersion: oldMetadata.storage.indexedDbSchemaVersion,
opfsProjectManifestSchemaVersion: oldMetadata.storage.opfsProjectManifestSchemaVersion,
workerAssets: oldMetadata.app.workerAssets,
},
next: {
productVersion: newMetadata.productVersion,
engineReleaseId: newMetadata.engineReleaseId,
indexedDbSchemaVersion: newMetadata.storage.indexedDbSchemaVersion,
opfsProjectManifestSchemaVersion: newMetadata.storage.opfsProjectManifestSchemaVersion,
workerAssets: newMetadata.app.workerAssets,
},
cache: {
document: newMetadata.app.documentCache,
manifest: newMetadata.app.manifestCache,
hashedAssets: newMetadata.app.hashedAssetCache,
},
switch: { failedPreflightKeptCurrent: true, atomicCurrentSwitch: true, oldReleaseRetained: true },
project: { sha256Before: projectBefore, sha256After: sha256File(projectFile), preserved: true },
};
fs.mkdirSync(reportRoot, { recursive: true });
fs.writeFileSync(path.join(reportRoot, "upgrade.json"), `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`upgrade-runbook-ok old=${oldMetadata.engineReleaseId} next=${newMetadata.engineReleaseId} idb=${oldMetadata.storage.indexedDbSchemaVersion}->${newMetadata.storage.indexedDbSchemaVersion} opfs=${newMetadata.storage.opfsProjectManifestSchemaVersion} workers=switched project=preserved\n`);
}
finally {
fs.rmSync(workspace, { recursive: true, force: true });
}