mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
The sync treats every lockfile crate without a source as ours and set the vendored sevenz-rust2 to the app version (7.0.5, 7.0.6), the same trap unrar fell into; cargo then wrote 0.23.0 back and CI's version check failed. It joins unrar on the vendored list, and the desktop lockfile is back to 0.23.0. Builds were unaffected: cargo always built the vendored source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ub1mez9eaVtTF8pMEFXvA9
235 lines
9.0 KiB
JavaScript
235 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* Version-sync tool. The VERSION file at the repo root is the single
|
|
* source of truth for the app version. Every build artifact (payload,
|
|
* desktop binary, npm package, Tauri bundle) derives its version from
|
|
* this one file via the script below.
|
|
*
|
|
* Usage
|
|
* node scripts/update-version.js Read VERSION, sync downstream files
|
|
* node scripts/update-version.js <x.y.z> Write x.y.z to VERSION, then sync
|
|
* node scripts/update-version.js --check Exit non-zero if any downstream file
|
|
* disagrees with VERSION
|
|
*
|
|
* Downstream targets
|
|
* - client/package.json (field: version)
|
|
* - client/package-lock.json (field: version + packages[""] .version)
|
|
* - client/src-tauri/tauri.conf.json (field: version)
|
|
* - client/src-tauri/Cargo.toml (line: `version = "..."` under [package])
|
|
* - payload/include/config.h (macro: PS5UPLOAD2_VERSION)
|
|
* - engine/Cargo.lock,
|
|
* client/src-tauri/Cargo.lock (version of each local workspace crate)
|
|
* - engine/Cargo.toml (line: `version = "..."` under
|
|
* [workspace.package]; all engine
|
|
* crates inherit it via
|
|
* `version.workspace = true`)
|
|
*
|
|
* The engine reports this same version from `/api/version`, so it stays on
|
|
* par with the client.
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const repoRoot = path.join(__dirname, "..");
|
|
const VERSION_FILE = path.join(repoRoot, "VERSION");
|
|
|
|
const args = process.argv.slice(2);
|
|
const checkMode = args.includes("--check");
|
|
const explicitVersion = args.find((a) => !a.startsWith("--"));
|
|
|
|
function fail(msg) {
|
|
console.error(`update-version: ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function readVersionFile() {
|
|
if (!fs.existsSync(VERSION_FILE)) fail("VERSION file missing at repo root");
|
|
return fs.readFileSync(VERSION_FILE, "utf8").trim();
|
|
}
|
|
|
|
function writeVersionFile(v) {
|
|
fs.writeFileSync(VERSION_FILE, `${v}\n`);
|
|
}
|
|
|
|
function looksLikeSemver(v) {
|
|
return /^\d+\.\d+\.\d+([-+][A-Za-z0-9._-]+)?$/.test(v);
|
|
}
|
|
|
|
// ─── Resolve target version ───────────────────────────────────────────────
|
|
|
|
let targetVersion;
|
|
if (explicitVersion) {
|
|
if (!looksLikeSemver(explicitVersion))
|
|
fail(`argument is not a semver: "${explicitVersion}"`);
|
|
if (checkMode) fail("--check cannot be combined with an explicit version");
|
|
targetVersion = explicitVersion;
|
|
writeVersionFile(targetVersion);
|
|
console.log(`VERSION → ${targetVersion}`);
|
|
} else {
|
|
targetVersion = readVersionFile();
|
|
if (!looksLikeSemver(targetVersion))
|
|
fail(`VERSION file contains "${targetVersion}" — not a semver`);
|
|
}
|
|
|
|
// ─── Patchers: each returns { path, current, desired, ok } ────────────────
|
|
|
|
function patchJsonField(relPath, fieldPath) {
|
|
const full = path.join(repoRoot, relPath);
|
|
if (!fs.existsSync(full)) return null;
|
|
const raw = fs.readFileSync(full, "utf8");
|
|
const json = JSON.parse(raw);
|
|
let current = json;
|
|
for (const key of fieldPath) {
|
|
if (current == null || typeof current !== "object") {
|
|
current = undefined;
|
|
break;
|
|
}
|
|
current = current[key];
|
|
}
|
|
if (typeof current !== "string") {
|
|
return { path: relPath, current: "<missing>", desired: targetVersion, ok: false };
|
|
}
|
|
const ok = current === targetVersion;
|
|
if (!ok && !checkMode) {
|
|
// Mutate nested path in place.
|
|
let cursor = json;
|
|
for (let i = 0; i < fieldPath.length - 1; i++) cursor = cursor[fieldPath[i]];
|
|
cursor[fieldPath[fieldPath.length - 1]] = targetVersion;
|
|
// Preserve trailing newline if original had one.
|
|
const trailing = raw.endsWith("\n") ? "\n" : "";
|
|
fs.writeFileSync(full, JSON.stringify(json, null, 2) + trailing);
|
|
}
|
|
return { path: relPath, current, desired: targetVersion, ok };
|
|
}
|
|
|
|
function patchRegex(relPath, regex, replacer, extractor) {
|
|
const full = path.join(repoRoot, relPath);
|
|
if (!fs.existsSync(full)) return null;
|
|
const raw = fs.readFileSync(full, "utf8");
|
|
const m = raw.match(regex);
|
|
const current = m ? extractor(m) : "<no-match>";
|
|
const ok = current === targetVersion;
|
|
if (!ok && !checkMode && m) {
|
|
fs.writeFileSync(full, raw.replace(regex, replacer));
|
|
}
|
|
return { path: relPath, current, desired: targetVersion, ok };
|
|
}
|
|
|
|
/**
|
|
* Patch the workspace crates' own versions inside a Cargo.lock.
|
|
*
|
|
* Cargo.lock records a version for every crate INCLUDING the local ones,
|
|
* so bumping Cargo.toml alone leaves the lock stale. Nothing fails
|
|
* loudly — cargo just silently rewrites the lock on the next build — so
|
|
* the symptom is that a freshly released, freshly cloned tree is dirty
|
|
* the moment anyone compiles it. That is what happened for v5.1.1.
|
|
*
|
|
* Local crates are exactly the `[[package]]` blocks with no `source`
|
|
* key: registry and git dependencies always carry one, path members
|
|
* never do. That rule needs no list of crate names to keep in sync.
|
|
*/
|
|
// Third-party crates we vendor and redirect to with `[patch.crates-io]`.
|
|
//
|
|
// They have no `source =` line in the lockfile (they resolve to a local
|
|
// path), which otherwise makes them look like one of our own crates and
|
|
// gets them "synced" to the ps5upload version — renaming unrar 0.5.8 to
|
|
// 5.3.1 and drifting the lockfile on every release. They keep upstream's
|
|
// version; only their vendored copy's own Cargo.toml governs it.
|
|
const VENDORED_THIRD_PARTY = new Set(["unrar", "sevenz-rust2"]);
|
|
|
|
function patchCargoLock(relPath) {
|
|
const full = path.join(repoRoot, relPath);
|
|
if (!fs.existsSync(full)) return null;
|
|
const raw = fs.readFileSync(full, "utf8");
|
|
const blocks = raw.split(/(?=\[\[package\]\])/);
|
|
let stale = 0;
|
|
const patched = blocks.map((block) => {
|
|
if (!block.startsWith("[[package]]")) return block;
|
|
if (/^source = /m.test(block)) return block; // registry or git crate
|
|
const nameMatch = block.match(/^name = "([^"]+)"/m);
|
|
if (nameMatch && VENDORED_THIRD_PARTY.has(nameMatch[1])) return block;
|
|
const m = block.match(/^version = "([^"]+)"/m);
|
|
if (!m || m[1] === targetVersion) return block;
|
|
stale++;
|
|
return block.replace(/^version = "[^"]+"/m, `version = "${targetVersion}"`);
|
|
});
|
|
const ok = stale === 0;
|
|
if (!ok && !checkMode) fs.writeFileSync(full, patched.join(""));
|
|
return {
|
|
path: relPath,
|
|
current: ok ? targetVersion : `${stale} local crate(s) stale`,
|
|
desired: targetVersion,
|
|
ok,
|
|
};
|
|
}
|
|
|
|
// ─── Run all patchers ─────────────────────────────────────────────────────
|
|
|
|
const results = [
|
|
patchJsonField("client/package.json", ["version"]),
|
|
patchJsonField("client/package-lock.json", ["version"]),
|
|
patchJsonField("client/package-lock.json", ["packages", "", "version"]),
|
|
patchJsonField("client/src-tauri/tauri.conf.json", ["version"]),
|
|
patchRegex(
|
|
"client/src-tauri/Cargo.toml",
|
|
/^version = "(\d+\.\d+\.\d+[^"]*)"/m,
|
|
`version = "${targetVersion}"`,
|
|
(m) => m[1],
|
|
),
|
|
patchRegex(
|
|
"payload/include/config.h",
|
|
/^#define PS5UPLOAD2_VERSION "(\d+\.\d+\.\d+[^"]*)"/m,
|
|
`#define PS5UPLOAD2_VERSION "${targetVersion}"`,
|
|
(m) => m[1],
|
|
),
|
|
// Engine workspace version. All engine crates inherit it via
|
|
// `version.workspace = true`, so this one line drives the binary's
|
|
// reported version (`/api/version`) and keeps the engine on par with
|
|
// the client. The `^version = "x.y.z"` line lives under
|
|
// `[workspace.package]`; dependency versions are inline (`serde = {
|
|
// version = ... }`) and not line-anchored, so they're never matched.
|
|
patchRegex(
|
|
"engine/Cargo.toml",
|
|
/^version = "(\d+\.\d+\.\d+[^"]*)"/m,
|
|
`version = "${targetVersion}"`,
|
|
(m) => m[1],
|
|
),
|
|
patchCargoLock("engine/Cargo.lock"),
|
|
patchCargoLock("client/src-tauri/Cargo.lock"),
|
|
].filter(Boolean);
|
|
|
|
// ─── Report ───────────────────────────────────────────────────────────────
|
|
|
|
const pad = results.reduce((n, r) => Math.max(n, r.path.length), 0);
|
|
const drift = [];
|
|
|
|
for (const r of results) {
|
|
const status = r.ok
|
|
? checkMode
|
|
? "ok"
|
|
: "ok (unchanged)"
|
|
: checkMode
|
|
? `DRIFT (has ${r.current})`
|
|
: `updated (was ${r.current})`;
|
|
if (!r.ok) drift.push(r);
|
|
console.log(` ${r.path.padEnd(pad)} ${status}`);
|
|
}
|
|
|
|
if (checkMode && drift.length > 0) {
|
|
console.error("");
|
|
console.error(
|
|
`update-version: ${drift.length} file(s) drifted from VERSION (${targetVersion}).`,
|
|
);
|
|
console.error("Run `node scripts/update-version.js` to sync.");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (checkMode) {
|
|
console.log(`All files match VERSION (${targetVersion}).`);
|
|
} else if (drift.length === 0) {
|
|
console.log(`All files already at ${targetVersion}; nothing to do.`);
|
|
} else {
|
|
console.log(`Synced ${drift.length} file(s) to ${targetVersion}.`);
|
|
}
|