import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; const source = readFileSync( join(dirname(fileURLToPath(import.meta.url)), "prepare-release.ps1"), "utf8", ).replace(/^\ufeff/, ""); const entry = source.lastIndexOf("try {\n Push-Location $RepoRoot"); const crlfEntry = source.lastIndexOf("try {\r\n Push-Location $RepoRoot"); const entryOffset = Math.max(entry, crlfEntry); assert.ok(entryOffset > 0); function run(cwd, command, args, ok = true) { const result = spawnSync(command, args, { cwd, encoding: "utf8", timeout: 60000, windowsHide: true, }); if (ok) assert.equal( result.status, 0, `${command}: ${result.stdout}\n${result.stderr}`, ); return result; } function fixture(t, build = "") { const root = mkdtempSync(join(tmpdir(), "proxywarden-release-test-")); t.after(() => { assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep)); assert.ok(root.includes("proxywarden-release-test-")); rmSync(root, { recursive: true, force: true }); }); const repo = join(root, "repo"); mkdirSync(repo); const write = (path, text) => { mkdirSync(dirname(join(repo, path)), { recursive: true }); writeFileSync(join(repo, path), text); }; const git = (...args) => run(repo, "git", args).stdout.trim(); write("package.json", '{"name":"proxywarden","version":"1.2.0"}\n'); write( "package-lock.json", '{"name":"proxywarden","version":"1.2.0","packages":{"":{"name":"proxywarden","version":"1.2.0"}}}\n', ); write("src-tauri/tauri.conf.json", '{"version":"1.2.0"}\n'); write( "src-tauri/Cargo.toml", '[package]\nname = "proxywarden"\nversion = "1.2.0"\n', ); write( "src-tauri/Cargo.lock", '[[package]]\nname = "proxywarden"\nversion = "1.2.0"\n', ); write(".gitignore", "node_modules/\nsrc-tauri/target/\nreleases/\n"); for (const cli of [ "typescript/bin/tsc", "vite/bin/vite.js", "@tauri-apps/cli/tauri.js", "prettier/bin/prettier.cjs", "eslint/bin/eslint.js", "vitest/vitest.mjs", ]) write(`node_modules/${cli}`, "fixture"); // Replace only expensive checks/build in this isolated copy. Git/version/artifact/push code is real. const stub = ` function Invoke-ReleaseChecks {} function Invoke-ReleaseBuild { ${build} $output = Join-Path $BundleRoot 'nsis' New-Item -ItemType Directory -Path $output -Force | Out-Null [IO.File]::WriteAllText((Join-Path $output "ProxyWarden_$($targetVersion)_x64-setup.exe"), 'test artifact') } `; write( "scripts/prepare-release.ps1", "\ufeff" + source.slice(0, entryOffset) + stub + source.slice(entryOffset), ); git("init", "-b", "master"); git("config", "user.name", "Release Test"); git("config", "user.email", "release-test@example.invalid"); git("config", "core.autocrlf", "false"); git("add", "."); git("commit", "-m", "initial"); const remote = join(root, "origin.git"); run(root, "git", ["init", "--bare", remote]); git("remote", "add", "origin", remote); git("push", "-u", "origin", "master"); const release = (...args) => run( repo, "pwsh", [ "-NoProfile", "-File", "scripts/prepare-release.ps1", "-Publish", ...args, ], false, ); const manifest = () => JSON.parse( readFileSync( join(repo, "releases/proxywarden-v1.2.1/release-manifest.json"), "utf8", ).replace(/^\ufeff/, ""), ); return { root, repo, remote, git, write, release, manifest }; } test("PlanOnly is offline and leaves versions/index/refs unchanged", (t) => { const f = fixture(t); f.git("remote", "set-url", "origin", join(f.root, "absent.git")); const before = f.git("status", "--porcelain"); const head = f.git("rev-parse", "HEAD"); const result = f.release("-PlanOnly"); assert.equal(result.status, 0, result.stderr); const plan = JSON.parse(result.stdout); assert.equal(plan.changed, false); assert.equal(plan.details.targetVersion, "1.2.1"); assert.equal(f.git("status", "--porcelain"), before); assert.equal(f.git("rev-parse", "HEAD"), head); }); test("release commits exact dirty source, versions both locks, tags and atomically pushes", (t) => { const f = fixture(t); f.write("feature.txt", "new feature"); const result = f.release("-Version", "1.2.1"); assert.equal(result.status, 0, result.stdout + result.stderr); const head = f.git("rev-parse", "HEAD"); assert.equal(f.git("rev-parse", "v1.2.1^{commit}"), head); assert.equal( f.git("ls-remote", "origin", "refs/heads/master").split(/\s/)[0], head, ); assert.equal(f.git("status", "--porcelain"), ""); assert.equal(f.manifest().gitCommit, head); assert.equal(f.manifest().gitRelease.status, "pushed"); assert.equal(f.manifest().artifacts.length, 1); assert.match( readFileSync(join(f.repo, "src-tauri/Cargo.lock"), "utf8"), /version = "1.2.1"/, ); const repeat = f.release("-Version", "1.2.1"); assert.notEqual(repeat.status, 0); assert.equal(f.git("rev-parse", "HEAD"), head); }); test("failed build creates no commit/tag/push and preserves existing staging", (t) => { const f = fixture(t, "throw 'Synthetic build failure'"); f.write("staged.txt", "staged"); f.git("add", "staged.txt"); const index = f.git("write-tree"), head = f.git("rev-parse", "HEAD"); assert.notEqual(f.release("-Version", "1.2.1").status, 0); assert.equal(f.git("write-tree"), index); assert.equal(f.git("rev-parse", "HEAD"), head); assert.equal(f.git("tag", "--list"), ""); }); test("source edit during build refuses to tag an artifact from another tree", (t) => { const f = fixture( t, "[IO.File]::WriteAllText((Join-Path $RepoRoot 'concurrent.txt'), 'changed during build')", ); const head = f.git("rev-parse", "HEAD"); const result = f.release("-Version", "1.2.1"); assert.notEqual(result.status, 0); assert.match(result.stderr, /changed during the build/); assert.match(result.stderr, /concurrent\.txt/); assert.equal(f.git("rev-parse", "HEAD"), head); assert.equal(f.git("tag", "--list"), ""); }); test("failed atomic push keeps artifacts and resumes without rebuilding", (t) => { const f = fixture(t); const hook = join(f.remote, "hooks/pre-receive"); writeFileSync(hook, "#!/bin/sh\nexit 1\n"); const before = f.git("ls-remote", "origin", "refs/heads/master"); const failed = f.release("-Version", "1.2.1"); assert.notEqual(failed.status, 0); assert.equal(f.manifest().gitRelease.status, "pending-push"); assert.equal(f.git("ls-remote", "origin", "refs/heads/master"), before); assert.equal(f.git("ls-remote", "origin", "refs/tags/v1.2.1"), ""); const commit = f.git("rev-parse", "HEAD"); rmSync(hook); const resumed = f.release("-Version", "1.2.1", "-Resume"); assert.equal(resumed.status, 0, resumed.stdout + resumed.stderr); assert.equal(f.git("rev-parse", "HEAD"), commit); assert.equal(f.manifest().gitRelease.status, "pushed"); f.write( "releases/proxywarden-v1.2.1/artifacts/nsis/ProxyWarden_1.2.1_x64-setup.exe", "tampered", ); assert.notEqual(f.release("-Version", "1.2.1", "-Resume").status, 0); }); test("remote-only version tag and diverged branch are refused before version edits", (t) => { const f = fixture(t); f.git("tag", "v1.2.1"); f.git("push", "origin", "refs/tags/v1.2.1"); f.git("tag", "-d", "v1.2.1"); const version = readFileSync(join(f.repo, "package.json"), "utf8"); assert.notEqual(f.release("-Version", "1.2.1").status, 0); assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); const clone = join(f.root, "other"); run(f.root, "git", ["clone", "--branch", "master", f.remote, clone]); run(clone, "git", ["config", "user.name", "Other"]); run(clone, "git", ["config", "user.email", "other@example.invalid"]); writeFileSync(join(clone, "remote-change.txt"), "remote"); run(clone, "git", ["add", "."]); run(clone, "git", ["commit", "-m", "remote change"]); run(clone, "git", ["push"]); const result = f.release("-Version", "1.2.2"); assert.notEqual(result.status, 0); assert.match(result.stderr, /Integrate them before releasing/); assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); }); test("invalid Windows versions and mismatched Cargo.lock fail without mutations", (t) => { const f = fixture(t); for (const version of ["01.2.3", "1.2.65536", "1.2.3-rc.1"]) assert.notEqual(f.release("-Version", version, "-PlanOnly").status, 0); f.write( "src-tauri/Cargo.lock", '[[package]]\nname = "proxywarden"\nversion = "0.0.0"\n', ); const result = f.release("-PlanOnly"); assert.notEqual(result.status, 0); assert.match(result.stderr, /Version mismatch/); }); test("unreachable origin reports the Git cause and diagnostic command without a PowerShell stack", (t) => { const f = fixture(t); f.git("remote", "set-url", "origin", join(f.root, "absent.git")); const head = f.git("rev-parse", "HEAD"); const version = readFileSync(join(f.repo, "package.json"), "utf8"); const result = f.release("-Version", "1.2.1"); assert.notEqual(result.status, 0); assert.match(result.stderr, /git ls-remote origin/); assert.match(result.stderr, /does not appear to be a git repository/); assert.doesNotMatch( result.stderr, /prepare-release\.ps1:\d|ScriptStackTrace|Line \|/, ); assert.equal(f.git("rev-parse", "HEAD"), head); assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); });