From 76d67ccdd35ad19c06e5426ec836d055a7205b74 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Thu, 29 Oct 2026 19:46:22 +1300 Subject: [PATCH 1/2] fix(cli): keep binary variants below wrapper versions --- .changeset/bun-global-update.md | 5 ++++ apps/cli/src/build.ts | 49 +++++++++++++++++---------------- 2 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 .changeset/bun-global-update.md diff --git a/.changeset/bun-global-update.md b/.changeset/bun-global-update.md new file mode 100644 index 0000000000..005103b7f8 --- /dev/null +++ b/.changeset/bun-global-update.md @@ -0,0 +1,5 @@ +--- +executor: patch +--- + +Keep platform-specific CLI binary package versions below the matching wrapper version in SemVer order. Package managers that compare all versions of `executor` will continue selecting the installable wrapper instead of a platform binary variant. diff --git a/apps/cli/src/build.ts b/apps/cli/src/build.ts index 6c1f701383..78a3c189d8 100644 --- a/apps/cli/src/build.ts +++ b/apps/cli/src/build.ts @@ -1,7 +1,7 @@ import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; -import { resolve, join, dirname } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import { $ } from "bun"; @@ -185,6 +185,19 @@ const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); * "linux-x64-musl", "darwin-arm64". */ const platformTag = (t: Target) => [platformName(t), t.arch, t.abi].filter(Boolean).join("-"); +/** Make a platform package version sort below its matching wrapper version. */ +const variantPackageVersion = (version: string, tag: string): string => { + const match = /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/.exec(version); + const baseVersion = match?.[1]; + if (baseVersion === undefined) { + throw new Error(`Invalid Executor package version for platform variant: ${version}`); + } + + const prerelease = match[2]; + const build = match[3]; + return `${baseVersion}-0${prerelease === undefined ? "" : `.${prerelease}`}.${tag}${build === undefined ? "" : `+${build}`}`; +}; + /** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build * artifact convention; the actual npm package name inside is `executor`. */ const targetPackageName = (t: Target) => `executor-${platformTag(t)}`; @@ -547,8 +560,11 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { // Variant package.json. All variants publish to the SAME npm package // name (`executor`) under platform-tagged versions (e.g. - // `1.4.14-linux-x64`). The wrapper references each variant via an - // `npm:` alias in its optionalDependencies. This mirrors codex's + // `2.0.0-0.beta.31.windows-x64`). The numeric leading prerelease + // identifier keeps each binary variant below the wrapper version in + // SemVer ordering, so package updaters don't mistake a variant for the + // globally installable package. The wrapper references each variant + // via an `npm:` alias in its optionalDependencies. This mirrors codex's // pattern and avoids ever having to claim a new npm package name when // a new platform is added — a single trusted-publishing config on the // `executor` package covers everything. @@ -556,7 +572,7 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { // No `bin` field on purpose — the wrapper's launcher resolves the // platform binary via require.resolve on the alias name and execs it. const tag = platformTag(target); - const variantVersion = `${meta.version}-${tag}`; + const variantVersion = variantPackageVersion(meta.version, tag); await writeFile( join(outDir, "package.json"), JSON.stringify( @@ -625,7 +641,7 @@ const buildWrapperPackage = async (binaries: Record) => { bin: { executor: "bin/executor" }, // Per-platform compiled binaries published as platform-tagged // versions of `executor` itself, referenced via npm:alias specs: - // "executor-linux-x64": "npm:executor@1.4.14-linux-x64" + // "executor-linux-x64": "npm:executor@1.4.14-0.linux-x64" // npm/bun fetch only the variant matching the current os/cpu (set // on each variant's package.json); everything else is a no-op. // The local alias name on the left is what the launcher passes to @@ -821,28 +837,14 @@ const publishPackedPackage = async (pkgDir: string, channel: string) => { throw new Error(`npm publish failed for ${pkgDir} (exit ${result.exitCode})`); }; -/** Extract the platform-tag suffix from a variant version string. - * e.g. "1.4.14-linux-x64" -> "linux-x64". - * Variants get a per-platform npm dist-tag (not `latest` or `beta`) so - * publishing them doesn't move the channel pointer. The wrapper alone - * drives `latest`/`beta`. */ -const variantTagFromVersion = (version: string): string => { - const idx = version.indexOf("-"); - if (idx === -1) { - throw new Error( - `Variant version missing platform-tag suffix (expected '-'): ${version}`, - ); - } - return version.slice(idx + 1); -}; - const publish = async (channel: string) => { const meta = await readMetadata(); // Variants publish first so the wrapper's optionalDependencies resolve. // All variants and the wrapper publish to the same npm package - // (`executor`) — variants under platform-tagged versions, wrapper under - // the channel tag. Single trusted-publishing config covers everything. + // (`executor`) — variants under platform-tagged versions and platform + // dist-tags, wrapper under the channel tag. Single trusted-publishing config + // covers everything. const platformDirs: string[] = []; for (const entry of new Bun.Glob("executor-*/package.json").scanSync({ cwd: distDir })) { platformDirs.push(join(distDir, dirname(entry))); @@ -858,8 +860,7 @@ const publish = async (channel: string) => { // (~5s per variant × 8 ≈ 40s). console.log(`Publishing ${platformDirs.length} platform variant(s)...`); for (const dir of platformDirs) { - const pkg = (await Bun.file(join(dir, "package.json")).json()) as { version: string }; - const tag = variantTagFromVersion(pkg.version); + const tag = basename(dir).slice("executor-".length); await publishPackedPackage(dir, tag); } From a5165320fbc978740a10e62ff42a24aaba30e963 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Thu, 29 Oct 2026 19:56:45 +1300 Subject: [PATCH 2/2] test(cli): cover platform variant version ordering --- apps/cli/src/build.ts | 16 ++------------ apps/cli/src/platform-variant-version.test.ts | 21 +++++++++++++++++++ apps/cli/src/platform-variant-version.ts | 17 +++++++++++++++ 3 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 apps/cli/src/platform-variant-version.test.ts create mode 100644 apps/cli/src/platform-variant-version.ts diff --git a/apps/cli/src/build.ts b/apps/cli/src/build.ts index 78a3c189d8..79b94ef7db 100644 --- a/apps/cli/src/build.ts +++ b/apps/cli/src/build.ts @@ -5,6 +5,7 @@ import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import { $ } from "bun"; +import { platformVariantVersion } from "./platform-variant-version"; import { WORKER_BUNDLER_DIRNAME, missingWorkerBundlerFiles } from "./worker-bundler-artifact"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); @@ -185,19 +186,6 @@ const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); * "linux-x64-musl", "darwin-arm64". */ const platformTag = (t: Target) => [platformName(t), t.arch, t.abi].filter(Boolean).join("-"); -/** Make a platform package version sort below its matching wrapper version. */ -const variantPackageVersion = (version: string, tag: string): string => { - const match = /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/.exec(version); - const baseVersion = match?.[1]; - if (baseVersion === undefined) { - throw new Error(`Invalid Executor package version for platform variant: ${version}`); - } - - const prerelease = match[2]; - const build = match[3]; - return `${baseVersion}-0${prerelease === undefined ? "" : `.${prerelease}`}.${tag}${build === undefined ? "" : `+${build}`}`; -}; - /** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build * artifact convention; the actual npm package name inside is `executor`. */ const targetPackageName = (t: Target) => `executor-${platformTag(t)}`; @@ -572,7 +560,7 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { // No `bin` field on purpose — the wrapper's launcher resolves the // platform binary via require.resolve on the alias name and execs it. const tag = platformTag(target); - const variantVersion = variantPackageVersion(meta.version, tag); + const variantVersion = platformVariantVersion(meta.version, tag); await writeFile( join(outDir, "package.json"), JSON.stringify( diff --git a/apps/cli/src/platform-variant-version.test.ts b/apps/cli/src/platform-variant-version.test.ts new file mode 100644 index 0000000000..2270f18432 --- /dev/null +++ b/apps/cli/src/platform-variant-version.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { platformVariantVersion } from "./platform-variant-version"; + +describe("platformVariantVersion", () => { + it("keeps a beta platform package below its wrapper version", () => { + expect(platformVariantVersion("2.0.0-beta.31", "windows-x64")).toBe( + "2.0.0-0.beta.31.windows-x64", + ); + }); + + it("keeps a stable platform package below its wrapper version", () => { + expect(platformVariantVersion("1.6.11", "linux-x64")).toBe("1.6.11-0.linux-x64"); + }); + + it("preserves build metadata", () => { + expect(platformVariantVersion("2.0.0-beta.31+build.4", "windows-x64")).toBe( + "2.0.0-0.beta.31.windows-x64+build.4", + ); + }); +}); diff --git a/apps/cli/src/platform-variant-version.ts b/apps/cli/src/platform-variant-version.ts new file mode 100644 index 0000000000..d40feaf735 --- /dev/null +++ b/apps/cli/src/platform-variant-version.ts @@ -0,0 +1,17 @@ +/** Create a platform package version below its matching CLI wrapper version. + * + * @param version - The installable wrapper's SemVer version. + * @param platform - The platform identifier appended to the wrapper release. + * @returns A prerelease version that package managers rank below the wrapper. + */ +export const platformVariantVersion = (version: string, platform: string): string => { + const match = /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/.exec(version); + if (match === null || match[1] === undefined) { + throw new Error(`Invalid Executor package version for platform variant: ${version}`); + } + + const baseVersion = match[1]; + const prerelease = match[2]; + const build = match[3]; + return `${baseVersion}-0${prerelease === undefined ? "" : `.${prerelease}`}.${platform}${build === undefined ? "" : `+${build}`}`; +};