From abb7490c1edd6335b2022bf43a7b32a91ed89e71 Mon Sep 17 00:00:00 2001 From: Edward Frazer Date: Tue, 28 Apr 2026 22:30:39 +0000 Subject: [PATCH] fix: split standalone installer staging from npm release flow --- .github/workflows/rust-release.yml | 12 +- scripts/stage_npm_packages.py | 55 --------- .../stage_standalone_installer_archives.py | 107 +++++++++++++++--- 3 files changed, 101 insertions(+), 73 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 1f1519df04..bdf8f04eb6 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -540,8 +540,16 @@ jobs: --release-version "$RELEASE_VERSION" \ --package codex \ --package codex-responses-api-proxy \ - --package codex-sdk \ - --standalone-output-dir dist/installer + --package codex-sdk + + - name: Stage standalone installer archives + env: + RELEASE_VERSION: ${{ steps.release_name.outputs.name }} + run: | + ./scripts/stage_standalone_installer_archives.py \ + --release-version "$RELEASE_VERSION" \ + --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --output-dir dist/installer - name: Stage installer scripts run: | diff --git a/scripts/stage_npm_packages.py b/scripts/stage_npm_packages.py index 5c7e38fbd9..3f75efc3ab 100755 --- a/scripts/stage_npm_packages.py +++ b/scripts/stage_npm_packages.py @@ -16,9 +16,6 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent BUILD_SCRIPT = REPO_ROOT / "codex-cli" / "scripts" / "build_npm_package.py" INSTALL_NATIVE_DEPS = REPO_ROOT / "codex-cli" / "scripts" / "install_native_deps.py" -STAGE_STANDALONE_INSTALLER_ARCHIVES = ( - REPO_ROOT / "scripts" / "stage_standalone_installer_archives.py" -) WORKFLOW_NAME = ".github/workflows/rust-release.yml" GITHUB_REPO = "openai/codex" @@ -56,12 +53,6 @@ def parse_args() -> argparse.Namespace: default=None, help="Directory where npm tarballs should be written (default: dist/npm).", ) - parser.add_argument( - "--standalone-output-dir", - type=Path, - default=None, - help="Directory where standalone installer archives should be written.", - ) parser.add_argument( "--keep-staging-dirs", action="store_true", @@ -139,26 +130,6 @@ def run_command(cmd: list[str]) -> None: subprocess.run(cmd, cwd=REPO_ROOT, check=True) -def stage_standalone_installer_archives( - release_version: str, - vendor_src: Path, - output_dir: Path, - packages: list[str], -) -> None: - cmd = [ - str(STAGE_STANDALONE_INSTALLER_ARCHIVES), - "--release-version", - release_version, - "--vendor-src", - str(vendor_src), - "--output-dir", - str(output_dir), - ] - for package in packages: - cmd.extend(["--package", package]) - run_command(cmd) - - def tarball_name_for_package(package: str, version: str) -> str: if package in CODEX_PLATFORM_PACKAGES: platform = package.removeprefix("codex-") @@ -222,32 +193,6 @@ def main() -> int: final_messages.append(f"Staged {package} at {pack_output}") - if args.standalone_output_dir is not None: - if vendor_src is None: - raise RuntimeError( - "--standalone-output-dir requires staged native binaries, but no selected " - "package installed native components." - ) - standalone_packages = [ - package for package in packages if package in CODEX_PLATFORM_PACKAGES - ] - if not standalone_packages: - raise RuntimeError( - "--standalone-output-dir requires at least one selected Codex platform " - "package." - ) - - standalone_output_dir = args.standalone_output_dir - standalone_output_dir.mkdir(parents=True, exist_ok=True) - stage_standalone_installer_archives( - args.release_version, - vendor_src, - standalone_output_dir, - standalone_packages, - ) - final_messages.append( - f"Staged standalone installer archives in {standalone_output_dir}" - ) finally: if vendor_temp_root is not None and not args.keep_staging_dirs: shutil.rmtree(vendor_temp_root, ignore_errors=True) diff --git a/scripts/stage_standalone_installer_archives.py b/scripts/stage_standalone_installer_archives.py index 27256ff0c0..e4d3cbc6c9 100755 --- a/scripts/stage_standalone_installer_archives.py +++ b/scripts/stage_standalone_installer_archives.py @@ -5,7 +5,10 @@ from __future__ import annotations import argparse import importlib.util +import json +import os import shutil +import subprocess import tarfile import tempfile from pathlib import Path @@ -13,6 +16,8 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent BUILD_SCRIPT = REPO_ROOT / "codex-cli" / "scripts" / "build_npm_package.py" +INSTALL_NATIVE_DEPS = REPO_ROOT / "codex-cli" / "scripts" / "install_native_deps.py" +WORKFLOW_NAME = ".github/workflows/rust-release.yml" _SPEC = importlib.util.spec_from_file_location("codex_build_npm_package", BUILD_SCRIPT) if _SPEC is None or _SPEC.loader is None: @@ -20,6 +25,12 @@ if _SPEC is None or _SPEC.loader is None: _BUILD_MODULE = importlib.util.module_from_spec(_SPEC) _SPEC.loader.exec_module(_BUILD_MODULE) CODEX_PLATFORM_PACKAGES = getattr(_BUILD_MODULE, "CODEX_PLATFORM_PACKAGES", {}) +STANDALONE_NATIVE_COMPONENTS = ( + "codex", + "codex-command-runner", + "codex-windows-sandbox-setup", + "rg", +) def parse_args() -> argparse.Namespace: @@ -32,9 +43,15 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--vendor-src", type=Path, - required=True, help="Directory containing native binaries under vendor/.", ) + parser.add_argument( + "--workflow-url", + help=( + "Optional workflow URL to download native artifacts from when --vendor-src " + "is not provided." + ), + ) parser.add_argument( "--output-dir", type=Path, @@ -51,7 +68,10 @@ def parse_args() -> argparse.Namespace: "Defaults to all platform packages." ), ) - return parser.parse_args() + args = parser.parse_args() + if args.vendor_src is None and not args.workflow_url: + parser.error("Provide either --vendor-src or --workflow-url.") + return args def archive_name(platform_tag: str, version: str) -> str: @@ -98,28 +118,83 @@ def write_archive(staging_dir: Path, output_path: Path) -> None: archive.add(path, arcname=path.relative_to(staging_dir), recursive=False) +def resolve_release_workflow(version: str) -> dict: + stdout = subprocess.check_output( + [ + "gh", + "run", + "list", + "--branch", + f"rust-v{version}", + "--json", + "workflowName,url,headSha", + "--workflow", + WORKFLOW_NAME, + "--jq", + "first(.[])", + ], + cwd=REPO_ROOT, + text=True, + ) + workflow = json.loads(stdout or "null") + if not workflow: + raise RuntimeError(f"Unable to find rust-release workflow for version {version}.") + return workflow + + +def resolve_workflow_url(version: str, override: str | None) -> str: + if override: + return override + workflow = resolve_release_workflow(version) + return workflow["url"] + + +def install_native_components(workflow_url: str, vendor_root: Path) -> Path: + cmd = [str(INSTALL_NATIVE_DEPS), "--workflow-url", workflow_url] + for component in STANDALONE_NATIVE_COMPONENTS: + cmd.extend(["--component", component]) + cmd.append(str(vendor_root)) + subprocess.run(cmd, cwd=REPO_ROOT, check=True) + return vendor_root / "vendor" + + def main() -> int: args = parse_args() - vendor_src = args.vendor_src.resolve() output_dir = args.output_dir.resolve() output_dir.mkdir(parents=True, exist_ok=True) packages = args.packages or sorted(CODEX_PLATFORM_PACKAGES) - for package in sorted(set(packages)): - package_config = CODEX_PLATFORM_PACKAGES[package] - platform_tag = package_config["npm_tag"] - target = package_config["target_triple"] - is_windows = package_config["os"] == "win32" - output_path = output_dir / archive_name(platform_tag, args.release_version) + runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())) - with tempfile.TemporaryDirectory( - prefix=f"codex-standalone-{platform_tag}-" - ) as staging_dir_str: - staging_dir = Path(staging_dir_str) - stage_target(vendor_src, staging_dir, target, is_windows) - write_archive(staging_dir, output_path) + vendor_temp_root: Path | None = None + try: + if args.vendor_src is not None: + vendor_src = args.vendor_src.resolve() + else: + workflow_url = resolve_workflow_url(args.release_version, args.workflow_url) + vendor_temp_root = Path( + tempfile.mkdtemp(prefix="standalone-native-", dir=runner_temp) + ) + vendor_src = install_native_components(workflow_url, vendor_temp_root) - print(f"Staged standalone installer archive at {output_path}") + for package in sorted(set(packages)): + package_config = CODEX_PLATFORM_PACKAGES[package] + platform_tag = package_config["npm_tag"] + target = package_config["target_triple"] + is_windows = package_config["os"] == "win32" + output_path = output_dir / archive_name(platform_tag, args.release_version) + + with tempfile.TemporaryDirectory( + prefix=f"codex-standalone-{platform_tag}-" + ) as staging_dir_str: + staging_dir = Path(staging_dir_str) + stage_target(vendor_src, staging_dir, target, is_windows) + write_archive(staging_dir, output_path) + + print(f"Staged standalone installer archive at {output_path}") + finally: + if vendor_temp_root is not None: + shutil.rmtree(vendor_temp_root, ignore_errors=True) return 0