From d9c014efce9c1e4632d15fdd87774e84c317e4ea Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 10 Feb 2026 10:33:47 -0800 Subject: [PATCH] # Use `@openai/codex` dist-tags for platform binaries instead of separate package names (#11339) https://github.com/openai/codex/pull/11318 introduced logic to publish platform artifacts as separate npm packages (for example, `@openai/codex-darwin-arm64`, `@openai/codex-linux-x64`, etc.). That requires provisioning and maintaining multiple package entries in npm, which we want to avoid. We still need to keep the package-size mitigation (platform-specific payloads), but we want that layout to live under a single npm package namespace (`@openai/codex`) using dist-tags. We also need to preserve pre-release workflows where users install `@openai/codex@alpha` and get platform-appropriate binaries. Additionally, we want GitHub Release assets to group Codex npm tarballs together, so platform tarballs should follow the same `codex-npm-*` filename prefix as the main Codex tarball. ## Release Strategy (New Scheme) We publish **one npm package name for Codex binaries** (`@openai/codex`) and use **dist-tags** to select platform-specific payloads. This avoids creating separate platform package names while keeping the package size split by platform. ### What gets published #### Mainline release (`x.y.z`) - `@openai/codex@latest` (meta package) - `@openai/codex@darwin-arm64` - `@openai/codex@darwin-x64` - `@openai/codex@linux-arm64` - `@openai/codex@linux-x64` - `@openai/codex@win32-arm64` - `@openai/codex@win32-x64` - `@openai/codex-responses-api-proxy@latest` - `@openai/codex-sdk@latest` #### Alpha release (`x.y.z-alpha.N`) - `@openai/codex@alpha` (meta package) - `@openai/codex@alpha-darwin-arm64` - `@openai/codex@alpha-darwin-x64` - `@openai/codex@alpha-linux-arm64` - `@openai/codex@alpha-linux-x64` - `@openai/codex@alpha-win32-arm64` - `@openai/codex@alpha-win32-x64` - `@openai/codex-responses-api-proxy@alpha` - `@openai/codex-sdk@alpha` As an example, the `package.json` for `@openai/codex@alpha` (using `0.99.0-alpha.17` as the `version`) would be: ``` { "name": "@openai/codex", "version": "0.99.0-alpha.17", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" }, "type": "module", "engines": { "node": ">=16" }, "files": [ "bin" ], "repository": { "type": "git", "url": "git+https://github.com/openai/codex.git", "directory": "codex-cli" }, "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", "optionalDependencies": { "@openai/codex-linux-x64": "npm:@openai/codex@0.99.0-alpha.17-linux-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.99.0-alpha.17-linux-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.99.0-alpha.17-darwin-x64", "@openai/codex-darwin-arm64": "npm:@openai/codex@0.99.0-alpha.17-darwin-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.99.0-alpha.17-win32-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.99.0-alpha.17-win32-arm64" } } ``` Note that the keys in `optionalDependencies` have "clean" names, but the values have the tag embedded. ### Important note **Note:** Because we never created the new platform package names on npm (for example, `@openai/codex-darwin-arm64`) since #11318 landed, there are no extra npm packages to clean up. ## What changed ### 1. Stage platform tarballs as `@openai/codex` with platform-specific versions File: `codex-cli/scripts/build_npm_package.py` - Added `CODEX_NPM_NAME = "@openai/codex"` and platform metadata `npm_tag` values: - `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-arm64`, `win32-x64` - For platform package staging (`codex-` inputs), switched generated `package.json` from: - `name = @openai/codex-` to: - `name = @openai/codex` - Added `compute_platform_package_version(version, platform_tag)` so platform tarballs have unique versions (`-`), which is required because npm forbids re-publishing the same `name@version`. ### 2. Point meta package optional dependencies at dist-tags on `@openai/codex` File: `codex-cli/scripts/build_npm_package.py` - Updated `optionalDependencies` generation for the main `codex` package to use npm alias syntax: - key remains alias package name (for example, `@openai/codex-darwin-arm64`) so runtime lookup behavior is unchanged - value now resolves to `@openai/codex` by dist-tag - Stable releases emit tags like `npm:@openai/codex@darwin-arm64`. - Alpha releases (`x.y.z-alpha.N`) emit tags like `npm:@openai/codex@alpha-darwin-arm64`. ### 3. Publish with per-tarball dist-tags in release CI File: `.github/workflows/rust-release.yml` - Reworked npm publish logic to derive the publish tag per tarball filename: - platform tarballs publish with `` tags for stable releases - platform tarballs publish with `alpha-` tags for alpha releases - top-level tarballs (`codex`, `codex-responses-api-proxy`, `codex-sdk`) continue using the existing channel tag policy (`latest` implicit for stable, `alpha` for alpha) - Added fail-fast behavior for unexpected tarball names to avoid silent mispublishes. ### 4. Normalize Codex platform tarball filenames for GitHub Release grouping Files: `scripts/stage_npm_packages.py`, `.github/workflows/rust-release.yml` - Renamed staged platform tarball filenames from: - `codex-linux--npm-.tgz` - `codex-darwin--npm-.tgz` - `codex-win32--npm-.tgz` - To: - `codex-npm-linux--.tgz` - `codex-npm-darwin--.tgz` - `codex-npm-win32--.tgz` This keeps all Codex npm artifacts grouped under a common `codex-npm-` prefix in GitHub Releases. ### 5. Documentation update File: `codex-cli/scripts/README.md` - Updated staging docs to clarify that platform-native variants are published as dist-tagged `@openai/codex` artifacts rather than separate npm package names. ## Resulting behavior - Mainline release: - `@openai/codex@latest` resolves the meta package - meta package optional dependencies resolve `@openai/codex@` - Alpha release: - users can continue installing `@openai/codex@alpha` - alpha meta package optional dependencies resolve `@openai/codex@alpha-` - Release assets: - Codex npm tarballs share `codex-npm-` prefix for cleaner grouping in GitHub Releases This preserves platform-specific payload distribution while avoiding separate npm package names and improves release-asset discoverability. ## Validation notes - Verified staged `package.json` output for stable and alpha meta packages includes expected alias targets. - Verified staged platform package manifests are `name=@openai/codex` with unique platform-suffixed versions. - Verified publish tag derivation maps renamed platform tarballs to expected stable and alpha dist-tags. --- .github/workflows/rust-release.yml | 38 +++++++++++++++++++++----- codex-cli/scripts/README.md | 3 +- codex-cli/scripts/build_npm_package.py | 26 ++++++++++++++++-- scripts/stage_npm_packages.py | 10 ++++++- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 193bcd859..5a6da68b7 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -595,9 +595,9 @@ jobs: mkdir -p dist/npm patterns=( "codex-npm-${version}.tgz" - "codex-linux-*-npm-${version}.tgz" - "codex-darwin-*-npm-${version}.tgz" - "codex-win32-*-npm-${version}.tgz" + "codex-npm-linux-*-${version}.tgz" + "codex-npm-darwin-*-${version}.tgz" + "codex-npm-win32-*-${version}.tgz" "codex-responses-api-proxy-npm-${version}.tgz" "codex-sdk-npm-${version}.tgz" ) @@ -615,20 +615,44 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - tag_args=() + prefix="" if [[ -n "${NPM_TAG}" ]]; then - tag_args+=(--tag "${NPM_TAG}") + prefix="${NPM_TAG}-" fi shopt -s nullglob - tarballs=(dist/npm/*-npm-"${VERSION}".tgz) + tarballs=(dist/npm/*-"${VERSION}".tgz) if [[ ${#tarballs[@]} -eq 0 ]]; then echo "No npm tarballs found in dist/npm for version ${VERSION}" exit 1 fi for tarball in "${tarballs[@]}"; do - npm publish "${GITHUB_WORKSPACE}/${tarball}" "${tag_args[@]}" + filename="$(basename "${tarball}")" + tag="" + + case "${filename}" in + codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) + platform="${filename#codex-npm-}" + platform="${platform%-${VERSION}.tgz}" + tag="${prefix}${platform}" + ;; + codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) + tag="${NPM_TAG}" + ;; + *) + echo "Unexpected npm tarball: ${filename}" + exit 1 + ;; + esac + + publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") + if [[ -n "${tag}" ]]; then + publish_cmd+=(--tag "${tag}") + fi + + echo "+ ${publish_cmd[*]}" + "${publish_cmd[@]}" done update-branch: diff --git a/codex-cli/scripts/README.md b/codex-cli/scripts/README.md index b9d55bad2..ca0d54b54 100644 --- a/codex-cli/scripts/README.md +++ b/codex-cli/scripts/README.md @@ -15,7 +15,8 @@ This downloads the native artifacts once, hydrates `vendor/` for each package, a tarballs to `dist/npm/`. When `--package codex` is provided, the staging helper builds the lightweight -`@openai/codex` meta package plus all `@openai/codex-` native packages. +`@openai/codex` meta package plus all platform-native `@openai/codex` variants +that are later published under platform-specific dist-tags. If you need to invoke `build_npm_package.py` directly, run `codex-cli/scripts/install_native_deps.py` first and pass `--vendor-src` pointing to the diff --git a/codex-cli/scripts/build_npm_package.py b/codex-cli/scripts/build_npm_package.py index 066f09e7e..6e1c677b3 100755 --- a/codex-cli/scripts/build_npm_package.py +++ b/codex-cli/scripts/build_npm_package.py @@ -14,40 +14,49 @@ CODEX_CLI_ROOT = SCRIPT_DIR.parent REPO_ROOT = CODEX_CLI_ROOT.parent RESPONSES_API_PROXY_NPM_ROOT = REPO_ROOT / "codex-rs" / "responses-api-proxy" / "npm" CODEX_SDK_ROOT = REPO_ROOT / "sdk" / "typescript" +CODEX_NPM_NAME = "@openai/codex" +# `npm_name` is the local optional-dependency alias consumed by `bin/codex.js`. +# The underlying package published to npm is always `@openai/codex`. CODEX_PLATFORM_PACKAGES: dict[str, dict[str, str]] = { "codex-linux-x64": { "npm_name": "@openai/codex-linux-x64", + "npm_tag": "linux-x64", "target_triple": "x86_64-unknown-linux-musl", "os": "linux", "cpu": "x64", }, "codex-linux-arm64": { "npm_name": "@openai/codex-linux-arm64", + "npm_tag": "linux-arm64", "target_triple": "aarch64-unknown-linux-musl", "os": "linux", "cpu": "arm64", }, "codex-darwin-x64": { "npm_name": "@openai/codex-darwin-x64", + "npm_tag": "darwin-x64", "target_triple": "x86_64-apple-darwin", "os": "darwin", "cpu": "x64", }, "codex-darwin-arm64": { "npm_name": "@openai/codex-darwin-arm64", + "npm_tag": "darwin-arm64", "target_triple": "aarch64-apple-darwin", "os": "darwin", "cpu": "arm64", }, "codex-win32-x64": { "npm_name": "@openai/codex-win32-x64", + "npm_tag": "win32-x64", "target_triple": "x86_64-pc-windows-msvc", "os": "win32", "cpu": "x64", }, "codex-win32-arm64": { "npm_name": "@openai/codex-win32-arm64", + "npm_tag": "win32-arm64", "target_triple": "aarch64-pc-windows-msvc", "os": "win32", "cpu": "arm64", @@ -244,6 +253,8 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None: package_json_path = CODEX_CLI_ROOT / "package.json" elif package in CODEX_PLATFORM_PACKAGES: platform_package = CODEX_PLATFORM_PACKAGES[package] + platform_npm_tag = platform_package["npm_tag"] + platform_version = compute_platform_package_version(version, platform_npm_tag) readme_src = REPO_ROOT / "README.md" if readme_src.exists(): @@ -253,8 +264,8 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None: codex_package_json = json.load(fh) package_json = { - "name": platform_package["npm_name"], - "version": version, + "name": CODEX_NPM_NAME, + "version": platform_version, "license": codex_package_json.get("license", "Apache-2.0"), "os": [platform_package["os"]], "cpu": [platform_package["cpu"]], @@ -294,7 +305,10 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None: if package == "codex": package_json["files"] = ["bin"] package_json["optionalDependencies"] = { - CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: version + CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: ( + f"npm:{CODEX_NPM_NAME}@" + f"{compute_platform_package_version(version, CODEX_PLATFORM_PACKAGES[platform_package]['npm_tag'])}" + ) for platform_package in PACKAGE_EXPANSIONS["codex"] if platform_package != "codex" } @@ -316,6 +330,12 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None: out.write("\n") +def compute_platform_package_version(version: str, platform_tag: str) -> str: + # npm forbids republishing the same package name/version, so each + # platform-specific tarball needs a unique version string. + return f"{version}-{platform_tag}" + + def run_command(cmd: list[str], cwd: Path | None = None) -> None: print("+", " ".join(cmd)) subprocess.run(cmd, cwd=cwd, check=True) diff --git a/scripts/stage_npm_packages.py b/scripts/stage_npm_packages.py index ff08111b9..5bbee755e 100755 --- a/scripts/stage_npm_packages.py +++ b/scripts/stage_npm_packages.py @@ -26,6 +26,7 @@ _BUILD_MODULE = importlib.util.module_from_spec(_SPEC) _SPEC.loader.exec_module(_BUILD_MODULE) PACKAGE_NATIVE_COMPONENTS = getattr(_BUILD_MODULE, "PACKAGE_NATIVE_COMPONENTS", {}) PACKAGE_EXPANSIONS = getattr(_BUILD_MODULE, "PACKAGE_EXPANSIONS", {}) +CODEX_PLATFORM_PACKAGES = getattr(_BUILD_MODULE, "CODEX_PLATFORM_PACKAGES", {}) def parse_args() -> argparse.Namespace: @@ -129,6 +130,13 @@ def run_command(cmd: list[str]) -> None: subprocess.run(cmd, cwd=REPO_ROOT, check=True) +def tarball_name_for_package(package: str, version: str) -> str: + if package in CODEX_PLATFORM_PACKAGES: + platform = package.removeprefix("codex-") + return f"codex-npm-{platform}-{version}.tgz" + return f"{package}-npm-{version}.tgz" + + def main() -> int: args = parse_args() @@ -160,7 +168,7 @@ def main() -> int: for package in packages: staging_dir = Path(tempfile.mkdtemp(prefix=f"npm-stage-{package}-", dir=runner_temp)) - pack_output = output_dir / f"{package}-npm-{args.release_version}.tgz" + pack_output = output_dir / tarball_name_for_package(package, args.release_version) cmd = [ str(BUILD_SCRIPT),