Advance M7 workflows and release operations
This commit is contained in:
@@ -17,6 +17,12 @@ fs.copyFileSync(path.join(root, "docs/web/third-party-notices.json"), path.join(
|
||||
fs.copyFileSync(path.join(root, "docs/web/sbom.spdx.json"), path.join(bundle, "sbom.spdx.json"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/deployment-contract.json"), path.join(bundle, "deployment-contract.json"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/DEPLOYMENT.md"), path.join(bundle, "DEPLOYMENT.md"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/operations-diagnostics.json"), path.join(bundle, "operations-diagnostics.json"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/RELEASE_NOTES.md"), path.join(bundle, "RELEASE_NOTES.md"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/KNOWN_LIMITATIONS.md"), path.join(bundle, "KNOWN_LIMITATIONS.md"));
|
||||
fs.copyFileSync(path.join(root, "docs/web/RELEASE_RECOVERY.md"), path.join(bundle, "RELEASE_RECOVERY.md"));
|
||||
fs.copyFileSync(path.join(root, "docs/WEB_BLENDER_MODELER_V1_SCOPE.md"), path.join(bundle, "V1_SCOPE.md"));
|
||||
fs.copyFileSync(path.join(root, "docs/status/parity-ledger.json"), path.join(bundle, "parity-ledger.json"));
|
||||
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/opensubdiv-source/LICENSE.txt"), path.join(bundle, "LICENSE-OpenSubdiv.txt"));
|
||||
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/gmp-source/COPYING.LESSERv3"), path.join(bundle, "LICENSE-GMP-LGPLv3.txt"));
|
||||
fs.writeFileSync(path.join(bundle, "README.txt"), [
|
||||
@@ -24,6 +30,7 @@ fs.writeFileSync(path.join(bundle, "README.txt"), [
|
||||
"",
|
||||
"Serve app/ from a static HTTP server that implements deployment-contract.json.",
|
||||
"See DEPLOYMENT.md for the required isolation, cache, MIME, ETag and byte-range behavior.",
|
||||
"See RELEASE_NOTES.md, KNOWN_LIMITATIONS.md, RELEASE_RECOVERY.md and V1_SCOPE.md for release boundaries and recovery.",
|
||||
"The application has no runtime CDN dependency.",
|
||||
"Opening index.html directly is unsupported because browsers restrict module Workers and WASM under file://.",
|
||||
"See SOURCE_OFFER.txt and third-party-notices.json for licensing and corresponding source.",
|
||||
@@ -37,6 +44,38 @@ fs.writeFileSync(path.join(bundle, "SOURCE_OFFER.txt"), [
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
function exportedInteger(file, name) {
|
||||
const source = fs.readFileSync(path.join(root, file), "utf8");
|
||||
const match = source.match(new RegExp(`export const ${name} = (\\d+);`));
|
||||
if (!match) throw new Error(`${file} does not export ${name} as an integer literal`);
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(root, "web/package.json"), "utf8"));
|
||||
const engineManifest = JSON.parse(fs.readFileSync(path.join(root, "web/app/public/engine-manifest.json"), "utf8"));
|
||||
const builtFiles = walk(path.join(root, "web/dist"));
|
||||
const workerAssets = builtFiles.filter((relative) => /^assets\/.+\.worker-[A-Za-z0-9_-]+\.js$/.test(relative));
|
||||
if (workerAssets.length === 0) throw new Error("production build has no content-hashed Worker assets");
|
||||
fs.writeFileSync(path.join(bundle, "release-metadata.json"), `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
product: "Web Blender Modeler V1",
|
||||
productVersion: packageJson.version,
|
||||
engineReleaseId: engineManifest.releaseId,
|
||||
storage: {
|
||||
indexedDbSchemaVersion: exportedInteger("web/app/src/storage/migrations.ts", "STORAGE_SCHEMA_VERSION"),
|
||||
opfsProjectManifestSchemaVersion: exportedInteger("web/app/src/storage/opfs-files.ts", "OPFS_PROJECT_SCHEMA_VERSION"),
|
||||
migrationDirection: "forward-only",
|
||||
originBound: true,
|
||||
},
|
||||
app: {
|
||||
workerAssets,
|
||||
entryAssets: builtFiles.filter((relative) => /^assets\/index-[A-Za-z0-9_-]+\.(?:js|css)$/.test(relative)),
|
||||
documentCache: "no-cache",
|
||||
manifestCache: "no-cache",
|
||||
hashedAssetCache: "public, max-age=31536000, immutable",
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
|
||||
function sha256(file) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
}
|
||||
@@ -53,18 +92,18 @@ function walk(directory, base = directory) {
|
||||
const manifest = walk(bundle).map((relative) => ({ path: relative, bytes: fs.statSync(path.join(bundle, relative)).size, sha256: sha256(path.join(bundle, relative)) }));
|
||||
fs.writeFileSync(path.join(bundle, "manifest.json"), `${JSON.stringify({ schemaVersion: 1, files: manifest }, null, 2)}\n`);
|
||||
|
||||
function deterministicArchive(output, cwd, entries) {
|
||||
function deterministicArchive(output, cwd, entries, options = []) {
|
||||
const tarPath = output.replace(/\.gz$/, "");
|
||||
fs.rmSync(output, { force: true });
|
||||
fs.rmSync(tarPath, { force: true });
|
||||
execFileSync("tar", ["--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", "-cf", tarPath, "-C", cwd, ...entries]);
|
||||
execFileSync("tar", ["--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", ...options, "-cf", tarPath, "-C", cwd, ...entries]);
|
||||
execFileSync("gzip", ["-n", "-f", tarPath]);
|
||||
}
|
||||
|
||||
const binaryArchive = path.join(releaseRoot, "blender-web-offline.tar.gz");
|
||||
deterministicArchive(binaryArchive, releaseRoot, ["blender-web-offline"]);
|
||||
const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz");
|
||||
deterministicArchive(sourceArchive, root, [
|
||||
const sourceEntries = [
|
||||
"blender-5.2.0",
|
||||
"web/app",
|
||||
"web/protocol",
|
||||
@@ -74,12 +113,58 @@ deterministicArchive(sourceArchive, root, [
|
||||
"web/eslint.config.js",
|
||||
"web/playwright.config.ts",
|
||||
"web/playwright.release.config.ts",
|
||||
"web/playwright.archive.config.ts",
|
||||
"web/playwright.single-thread.config.ts",
|
||||
"tools/web",
|
||||
"docs/web",
|
||||
"docs/status",
|
||||
"docs/PROJECT_STATUS_AND_NEXT_WORK.md",
|
||||
"docs/WEB_BLENDER_MODELER_V1_SCOPE.md",
|
||||
"docs/status/parity-ledger.json",
|
||||
"WEB_BLENDER_MIGRATION_EXECUTION_PLAN.md",
|
||||
]);
|
||||
];
|
||||
|
||||
function sourceManifestFiles(entries) {
|
||||
const files = [];
|
||||
const visit = (absolute, relative) => {
|
||||
const stat = fs.lstatSync(absolute);
|
||||
if (stat.isDirectory()) {
|
||||
if (path.basename(absolute) === "__pycache__") return;
|
||||
for (const name of fs.readdirSync(absolute).sort((left, right) => left.localeCompare(right))) {
|
||||
visit(path.join(absolute, name), path.posix.join(relative, name));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (relative.endsWith(".pyc")) return;
|
||||
if (stat.isSymbolicLink()) {
|
||||
files.push({ path: relative, type: "symlink", target: fs.readlinkSync(absolute) });
|
||||
return;
|
||||
}
|
||||
files.push({ path: relative, type: "file", bytes: stat.size, sha256: sha256(absolute) });
|
||||
};
|
||||
for (const entry of entries) visit(path.join(root, entry), entry);
|
||||
return files;
|
||||
}
|
||||
|
||||
const sourceManifestPath = path.join(root, "SOURCE_MANIFEST.json");
|
||||
if (fs.existsSync(sourceManifestPath)) throw new Error(`refusing to replace existing ${sourceManifestPath}`);
|
||||
const sourceManifest = {
|
||||
schemaVersion: 1,
|
||||
product: "Web Blender Modeler V1",
|
||||
version: packageJson.version,
|
||||
engineReleaseId: engineManifest.releaseId,
|
||||
files: sourceManifestFiles(sourceEntries),
|
||||
};
|
||||
fs.writeFileSync(sourceManifestPath, `${JSON.stringify(sourceManifest, null, 2)}\n`);
|
||||
try {
|
||||
deterministicArchive(sourceArchive, root, [...sourceEntries, "SOURCE_MANIFEST.json"], [
|
||||
"--exclude=*/__pycache__",
|
||||
"--exclude=*/__pycache__/*",
|
||||
"--exclude=*.pyc",
|
||||
]);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(sourceManifestPath, { force: true });
|
||||
}
|
||||
const sums = [binaryArchive, sourceArchive].map((file) => `${sha256(file)} ${path.basename(file)}`).join("\n") + "\n";
|
||||
fs.writeFileSync(path.join(releaseRoot, "SHA256SUMS.txt"), sums);
|
||||
process.stdout.write(`offline-release-ok binary=${fs.statSync(binaryArchive).size} source=${fs.statSync(sourceArchive).size} sha256=${sha256(binaryArchive)}\n`);
|
||||
|
||||
Reference in New Issue
Block a user