From 18ad36ac67f084c687b69ce77781e4b20d9a6af2 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sun, 10 May 2026 19:49:53 -0400 Subject: [PATCH] feat(release): publish Windows prebuilt artifacts --- .github/workflows/ci.yml | 1 + .github/workflows/release-prebuilt-npm.yml | 2 + CHANGELOG.md | 2 + bin/hunk.cjs | 4 ++ scripts/build-prebuilt-artifact.ts | 12 +++- scripts/prebuilt-package-helpers.test.ts | 19 ++++++ scripts/prebuilt-package-helpers.ts | 2 +- scripts/smoke-prebuilt-install.ts | 68 +++++++++++++--------- 8 files changed, 77 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70cfa03c..0d44e63c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,7 @@ jobs: os: - ubuntu-latest - macos-latest + - windows-latest steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/release-prebuilt-npm.yml b/.github/workflows/release-prebuilt-npm.yml index 3641f511..9bd86447 100644 --- a/.github/workflows/release-prebuilt-npm.yml +++ b/.github/workflows/release-prebuilt-npm.yml @@ -36,6 +36,8 @@ jobs: runner: ubuntu-24.04-arm - package_name: hunkdiff-linux-x64 runner: ubuntu-latest + - package_name: hunkdiff-windows-x64 + runner: windows-latest - package_name: hunkdiff-darwin-x64 runner: macos-15-intel - package_name: hunkdiff-darwin-arm64 diff --git a/CHANGELOG.md b/CHANGELOG.md index b41dc8fe..06c7e176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable user-visible changes to Hunk are documented in this file. ### Added +- Added Windows x64 prebuilt artifact publishing to the release workflow. + ### Changed ### Fixed diff --git a/bin/hunk.cjs b/bin/hunk.cjs index 84728170..19e94359 100755 --- a/bin/hunk.cjs +++ b/bin/hunk.cjs @@ -65,6 +65,10 @@ function hostCandidates() { if (arch === "x64") return [{ packageName: "hunkdiff-linux-x64", binary }]; } + if (platform === "windows") { + if (arch === "x64") return [{ packageName: "hunkdiff-windows-x64", binary }]; + } + return []; } diff --git a/scripts/build-prebuilt-artifact.ts b/scripts/build-prebuilt-artifact.ts index d64b07ba..8caf4536 100644 --- a/scripts/build-prebuilt-artifact.ts +++ b/scripts/build-prebuilt-artifact.ts @@ -36,7 +36,11 @@ const repoRoot = path.resolve(import.meta.dir, ".."); const options = parseArgs(process.argv.slice(2)); const spec = getHostPlatformPackageSpec(); const binaryName = binaryFilenameForSpec(spec); -const compiledBinary = path.join(repoRoot, "dist", "hunk"); +const compiledBinaryCandidates = [ + path.join(repoRoot, "dist", binaryName), + path.join(repoRoot, "dist", "hunk"), +]; +const compiledBinary = compiledBinaryCandidates.find((candidate) => existsSync(candidate)); const outputRoot = path.resolve(options.outputRoot ?? releaseArtifactsDir(repoRoot)); const outputDir = path.join(outputRoot, spec.packageName); @@ -46,8 +50,10 @@ if (options.expectedPackage && options.expectedPackage !== spec.packageName) { ); } -if (!existsSync(compiledBinary)) { - throw new Error(`Missing compiled binary at ${compiledBinary}. Run \`bun run build:bin\` first.`); +if (!compiledBinary) { + throw new Error( + `Missing compiled binary at ${compiledBinaryCandidates.join(" or ")}. Run \`bun run build:bin\` first.`, + ); } rmSync(outputDir, { recursive: true, force: true }); diff --git a/scripts/prebuilt-package-helpers.test.ts b/scripts/prebuilt-package-helpers.test.ts index 5dc6d13a..5df40b38 100644 --- a/scripts/prebuilt-package-helpers.test.ts +++ b/scripts/prebuilt-package-helpers.test.ts @@ -75,6 +75,7 @@ describe("prebuilt package helpers", () => { expect(getPlatformPackageSpecForHost("linux", "arm64").packageName).toBe( "hunkdiff-linux-arm64", ); + expect(getPlatformPackageSpecForHost("win32", "x64").packageName).toBe("hunkdiff-windows-x64"); }); test("getHostPlatformPackageSpec resolves the current machine", () => { @@ -102,6 +103,24 @@ describe("prebuilt package helpers", () => { expect(manifest.cpu).toEqual(["x64"]); }); + test("buildPlatformPackageManifest maps Windows packages to npm win32", () => { + const manifest = buildPlatformPackageManifest( + { + version: "1.2.3", + description: "Desktop diff viewer", + license: "MIT", + }, + getPlatformPackageSpecForHost("win32", "x64"), + ); + + expect(manifest.name).toBe("hunkdiff-windows-x64"); + expect(manifest.bin).toEqual({ + hunk: "./bin/hunk.exe", + }); + expect(manifest.os).toEqual(["win32"]); + expect(manifest.cpu).toEqual(["x64"]); + }); + test("sortPlatformPackageSpecs keeps package publish order stable", () => { const reversed = [...PLATFORM_PACKAGE_MATRIX].reverse(); expect(sortPlatformPackageSpecs(reversed).map((spec) => spec.packageName)).toEqual([ diff --git a/scripts/prebuilt-package-helpers.ts b/scripts/prebuilt-package-helpers.ts index b71f6d29..5e1b3686 100644 --- a/scripts/prebuilt-package-helpers.ts +++ b/scripts/prebuilt-package-helpers.ts @@ -25,7 +25,7 @@ const ARCH_NAME_MAP: Partial> = { arm64: "arm64", }; -/** Platforms we actually plan to publish in the first prebuilt-binary rollout. */ +/** Platforms published as optional prebuilt binary packages. */ export const PLATFORM_PACKAGE_MATRIX: PlatformPackageSpec[] = [ { packageName: "hunkdiff-darwin-arm64", diff --git a/scripts/smoke-prebuilt-install.ts b/scripts/smoke-prebuilt-install.ts index 2266e29f..79c46d7c 100644 --- a/scripts/smoke-prebuilt-install.ts +++ b/scripts/smoke-prebuilt-install.ts @@ -38,6 +38,30 @@ function run(command: string[], options?: { cwd?: string; env?: NodeJS.ProcessEn return { stdout, stderr }; } +/** Resolve a command path for a sanitized PATH that still works cross-platform. */ +function commandPath(command: string) { + const proc = Bun.spawnSync( + process.platform === "win32" ? ["where", command] : ["bash", "-lc", `command -v ${command}`], + { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + env: process.env, + }, + ); + const resolved = Buffer.from(proc.stdout).toString("utf8").split(/\r?\n/, 1)[0]?.trim(); + if (proc.exitCode !== 0 || !resolved) { + throw new Error(`Could not resolve ${command} on PATH for the prebuilt install smoke test.`); + } + + return resolved; +} + +/** Resolve a command directory for a sanitized PATH that still works cross-platform. */ +function commandDirectory(command: string) { + return path.dirname(commandPath(command)); +} + const repoRoot = path.resolve(import.meta.dir, ".."); const packageVersion = JSON.parse(await Bun.file(path.join(repoRoot, "package.json")).text()) .version as string; @@ -54,28 +78,9 @@ try { installDir = mkdtempSync(path.join(tempRoot, "hunk-prebuilt-install-")); smokeMetaDir = mkdtempSync(path.join(tempRoot, "hunk-prebuilt-meta-")); - const nodeBinary = Bun.spawnSync(["bash", "-lc", "command -v node"], { - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - env: process.env, - }); - const resolvedNode = Buffer.from(nodeBinary.stdout).toString("utf8").trim(); - if (nodeBinary.exitCode !== 0 || resolvedNode.length === 0) { - throw new Error("Could not resolve node on PATH for the prebuilt install smoke test."); - } - const bashBinary = Bun.spawnSync(["bash", "-lc", "command -v bash"], { - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - env: process.env, - }); - const resolvedBash = Buffer.from(bashBinary.stdout).toString("utf8").trim(); - if (bashBinary.exitCode !== 0 || resolvedBash.length === 0) { - throw new Error("Could not resolve bash on PATH for the prebuilt install smoke test."); - } - const nodeDir = path.dirname(resolvedNode); - const bashDir = path.dirname(resolvedBash); + const nodePath = commandPath("node"); + const nodeDir = path.dirname(nodePath); + const bashDir = commandDirectory("bash"); run(["npm", "pack", "--pack-destination", packageDir], { cwd: path.join(releaseRoot, hostSpec.packageName), @@ -104,13 +109,18 @@ try { run(["npm", "install", "-g", "--prefix", installDir, metaTarball]); - const sanitizedPath = [path.join(installDir, "bin"), nodeDir, bashDir].join(":"); - const installedHunk = path.join(installDir, "bin", "hunk"); + const installedBinDir = process.platform === "win32" ? installDir : path.join(installDir, "bin"); + const installedPackageRoot = + process.platform === "win32" + ? path.join(installDir, "node_modules", "hunkdiff") + : path.join(installDir, "lib", "node_modules", "hunkdiff"); + const sanitizedPath = [installedBinDir, nodeDir, bashDir].join(path.delimiter); + const installedHunk = path.join( + installedBinDir, + process.platform === "win32" ? "hunk.cmd" : "hunk", + ); const installedPlatformBinary = path.join( - installDir, - "lib", - "node_modules", - "hunkdiff", + installedPackageRoot, "node_modules", hostSpec.packageName, "bin", @@ -161,7 +171,7 @@ try { const bunCheck = Bun.spawnSync( [ - resolvedNode, + nodePath, "-e", "const {spawnSync}=require('node:child_process'); process.exit(spawnSync('bun',['--version'],{stdio:'ignore'}).status===0?1:0);", ],