Advance M7 workflows and release operations
This commit is contained in:
117
tools/web/check-upgrade-runbook.mjs
Normal file
117
tools/web/check-upgrade-runbook.mjs
Normal file
@@ -0,0 +1,117 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user