118 lines
6.5 KiB
JavaScript
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 });
|
|
}
|