fix: split standalone installer staging from npm release flow

This commit is contained in:
Edward Frazer
2026-04-28 22:30:39 +00:00
parent 35397a43e2
commit abb7490c1e
3 changed files with 101 additions and 73 deletions

View File

@@ -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: |

View File

@@ -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)

View File

@@ -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/<target>.",
)
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