mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-18 18:10:05 -05:00
kill-stale-client did `pkill -f ps5upload-engine`, which SIGTERMed every engine on the machine — a standalone one on another port died 70 minutes into a package build. It now stops only an engine listening on 19113 (an engine whose app is gone exits by itself). The decision is a small pure module with node:test cases, which scripts:check now runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
189 lines
7.0 KiB
JavaScript
Executable File
189 lines
7.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, "..");
|
|
const skipParts = new Set([
|
|
".git",
|
|
"node_modules",
|
|
"client/node_modules",
|
|
"target",
|
|
"engine/target",
|
|
"client/src-tauri/target",
|
|
"dist",
|
|
"client/dist",
|
|
"coverage",
|
|
"client/coverage",
|
|
"bench/fixtures",
|
|
"bench/reports",
|
|
]);
|
|
|
|
function walk(dir, out = []) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
const rel = path.relative(repoRoot, full).replaceAll(path.sep, "/");
|
|
if ([...skipParts].some((part) => rel === part || rel.startsWith(`${part}/`))) {
|
|
continue;
|
|
}
|
|
if (entry.isDirectory()) walk(full, out);
|
|
else out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function run(label, cmd, args) {
|
|
const res = spawnSync(cmd, args, {
|
|
cwd: repoRoot,
|
|
stdio: "pipe",
|
|
encoding: "utf8",
|
|
});
|
|
if (res.status !== 0) {
|
|
process.stderr.write(`\n[check-scripts] ${label} failed\n`);
|
|
if (res.stdout) process.stderr.write(res.stdout);
|
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function hasCommand(cmd) {
|
|
if (process.platform === "win32") {
|
|
return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
|
|
}
|
|
return spawnSync("sh", ["-c", "command -v \"$1\" >/dev/null 2>&1", "sh", cmd], {
|
|
stdio: "ignore",
|
|
}).status === 0;
|
|
}
|
|
|
|
function checkSdkPin() {
|
|
const metadataPath = path.join(repoRoot, "scripts", "ps5-sdk.env");
|
|
const metadataText = fs.readFileSync(metadataPath, "utf8");
|
|
const metadata = Object.fromEntries(
|
|
metadataText
|
|
.split(/\r?\n/)
|
|
.filter((line) => /^[A-Z0-9_]+=/.test(line))
|
|
.map((line) => {
|
|
const split = line.indexOf("=");
|
|
return [line.slice(0, split), line.slice(split + 1)];
|
|
}),
|
|
);
|
|
const failures = [];
|
|
if (!/^v\d+\.\d+(?:\.\d+)?$/.test(metadata.PS5_SDK_TAG || "")) {
|
|
failures.push("PS5_SDK_TAG must be a v-prefixed release number");
|
|
}
|
|
for (const key of ["PS5_SDK_SHA256", "PS5_SDK_NID_SHA256"]) {
|
|
if (!/^[0-9a-f]{64}$/.test(metadata[key] || "")) {
|
|
failures.push(`${key} must be a lowercase SHA-256`);
|
|
}
|
|
}
|
|
|
|
const nidPath = path.join(repoRoot, "scripts", "ps5-sdk", "prospero-nid");
|
|
const nidHash = createHash("sha256").update(fs.readFileSync(nidPath)).digest("hex");
|
|
if (nidHash !== metadata.PS5_SDK_NID_SHA256) {
|
|
failures.push("portable prospero-nid does not match PS5_SDK_NID_SHA256");
|
|
}
|
|
|
|
const requiredReferences = new Map([
|
|
["scripts/install-macos.sh", "scripts/install-ps5-sdk.sh"],
|
|
["scripts/install-ubuntu.sh", "scripts/install-ps5-sdk.sh"],
|
|
["scripts/install-windows.ps1", "ps5-sdk.env"],
|
|
[".github/workflows/engine-ci.yml", "scripts/install-ps5-sdk.sh"],
|
|
[".github/workflows/publish.yml", "scripts/install-ps5-sdk.sh"],
|
|
]);
|
|
for (const [file, needle] of requiredReferences) {
|
|
const content = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
|
if (!content.includes(needle)) failures.push(`${file} does not use ${needle}`);
|
|
if (content.includes("PS5_SDK_TAG: v0.")) failures.push(`${file} contains a second hard-coded SDK pin`);
|
|
}
|
|
|
|
const currentFiles = [
|
|
"Makefile",
|
|
"README.md",
|
|
"FAQ.md",
|
|
...requiredReferences.keys(),
|
|
];
|
|
for (const file of currentFiles) {
|
|
const content = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
|
if (content.includes("SDK v0.41") || content.includes("SDK_TAG=\"v0.41\"")) {
|
|
failures.push(`${file} still references the previous SDK v0.41 pin`);
|
|
}
|
|
}
|
|
for (const file of ["README.md", "FAQ.md"]) {
|
|
const content = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
|
if (!content.includes(`SDK ${metadata.PS5_SDK_TAG}`)) {
|
|
failures.push(`${file} does not document the pinned SDK ${metadata.PS5_SDK_TAG}`);
|
|
}
|
|
}
|
|
|
|
const publishWorkflow = fs.readFileSync(
|
|
path.join(repoRoot, ".github", "workflows", "publish.yml"),
|
|
"utf8",
|
|
);
|
|
const dpiArtifact = "payload/installer/ps5upload-installer.elf.gz";
|
|
const dpiReferences = publishWorkflow.split(dpiArtifact).length - 1;
|
|
if (dpiReferences < 3) {
|
|
failures.push(
|
|
"publish.yml must build, upload, and verify the PS5Upload installer gzip for release clients",
|
|
);
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
process.stderr.write("\n[check-scripts] PS5 SDK pin validation failed\n");
|
|
for (const failure of failures) process.stderr.write(` - ${failure}\n`);
|
|
return false;
|
|
}
|
|
process.stdout.write(
|
|
`[check-scripts] PS5 SDK pin ok (${metadata.PS5_SDK_TAG}, ${metadata.PS5_SDK_SHA256})\n`,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
const files = walk(repoRoot);
|
|
const nodeFiles = files.filter((f) => /\.(mjs|js)$/i.test(f));
|
|
const shellFiles = files.filter((f) => /\.sh$/i.test(f));
|
|
const pythonFiles = files.filter((f) => /\.py$/i.test(f));
|
|
const psFiles = files.filter((f) => /\.ps1$/i.test(f));
|
|
|
|
let ok = true;
|
|
ok = checkSdkPin() && ok;
|
|
for (const f of nodeFiles) ok = run(path.relative(repoRoot, f), "node", ["--check", f]) && ok;
|
|
// Scripts with logic worth pinning carry a node:test file beside them.
|
|
const nodeTests = nodeFiles.filter((f) => f.endsWith(".test.mjs"));
|
|
if (nodeTests.length) ok = run("node --test", "node", ["--test", ...nodeTests]) && ok;
|
|
if (hasCommand("bash")) {
|
|
for (const f of shellFiles) ok = run(path.relative(repoRoot, f), "bash", ["-n", f]) && ok;
|
|
} else if (shellFiles.length > 0) {
|
|
process.stdout.write("[check-scripts] bash not found; skipped shell parser checks\n");
|
|
}
|
|
if (hasCommand("python3")) {
|
|
for (const f of pythonFiles) ok = run(path.relative(repoRoot, f), "python3", ["-m", "py_compile", f]) && ok;
|
|
} else if (pythonFiles.length > 0) {
|
|
process.stdout.write("[check-scripts] python3 not found; skipped Python parser checks\n");
|
|
}
|
|
|
|
const pwsh = spawnSync("pwsh", ["-NoProfile", "-Command", "$PSVersionTable.PSVersion"], {
|
|
stdio: "ignore",
|
|
});
|
|
if (pwsh.status === 0) {
|
|
for (const f of psFiles) {
|
|
// PowerShell's `[ref]` requires the variable to exist before being
|
|
// passed by reference — calling `[ref]$errors` against an
|
|
// undeclared `$errors` produces "[ref] cannot be applied to a
|
|
// variable that does not exist" at runtime even before the parser
|
|
// executes. Declare both `$tokens` and `$errors` up front so the
|
|
// ParseFile output parameters land in real variables.
|
|
const escaped = f.replaceAll("'", "''");
|
|
const cmd = `$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('${escaped}', [ref]$tokens, [ref]$errors) > $null; if ($errors.Count) { $errors | ForEach-Object { Write-Error $_ }; exit 1 }`;
|
|
ok = run(path.relative(repoRoot, f), "pwsh", ["-NoProfile", "-Command", cmd]) && ok;
|
|
}
|
|
} else if (psFiles.length > 0) {
|
|
process.stdout.write("[check-scripts] pwsh not found; skipped PowerShell parser checks\n");
|
|
}
|
|
|
|
if (!ok) process.exit(1);
|
|
process.stdout.write(
|
|
`[check-scripts] ok (${nodeFiles.length} node, ${shellFiles.length} shell, ${pythonFiles.length} python, ${psFiles.length} powershell)\n`,
|
|
);
|