Support alpha hotfix release versions (#34463)

## What changed

- Map Python `aN.postM` versions to Codex `-alpha.N.M` release tags through shared release-version helpers.
- Accept alpha hotfix versions in Python runtime workflows, Rust release validation, npm publishing, and the shell and PowerShell installers.

## Testing

- Cover version conversion, workflow output, runtime setup, artifact staging, and installer handling for alpha hotfix releases.

GitOrigin-RevId: b95edb56f7c93b435a8050c10f0202dc117e6669
This commit is contained in:
Michael Bolin
2026-07-21 03:54:37 +00:00
committed by copyberry
parent a30aee8d90
commit 9970cd706f
10 changed files with 193 additions and 74 deletions

View File

@@ -13,6 +13,15 @@ import urllib.error
import urllib.request
from pathlib import Path
_SDK_PYTHON_ROOT = str(Path(__file__).resolve().parent)
if _SDK_PYTHON_ROOT not in sys.path:
sys.path.insert(0, _SDK_PYTHON_ROOT)
from release_version import ( # noqa: E402
codex_release_tag as _release_tag,
normalize_codex_version as _normalized_package_version,
)
PACKAGE_NAME = "openai-codex-cli-bin"
SDK_PACKAGE_NAME = "openai-codex"
REPO_SLUG = "openai/codex"
@@ -326,34 +335,6 @@ def _github_token() -> str | None:
return None
def _normalized_package_version(version: str) -> str:
normalized = version.strip()
if normalized.startswith("rust-v"):
normalized = normalized.removeprefix("rust-v")
elif normalized.startswith("v"):
normalized = normalized.removeprefix("v")
normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized)
normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized)
normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized)
return normalized
def _codex_release_version(version: str) -> str:
normalized = _normalized_package_version(version)
match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)*)(a|b|rc)([0-9]+)", normalized)
if match is None:
return normalized
base, prerelease, number = match.groups()
prerelease_name = {"a": "alpha", "b": "beta", "rc": "rc"}[prerelease]
return f"{base}-{prerelease_name}.{number}"
def _release_tag(version: str) -> str:
return f"rust-v{_codex_release_version(version)}"
def _source_tree_runtime_dependency_version() -> str | None:
"""Read the runtime dependency pin when the SDK is running from a checkout."""
pyproject_path = Path(__file__).resolve().parent / "pyproject.toml"

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from typing import Sequence
_PYTHON_RUNTIME_VERSION_PATTERN = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:a[0-9]+(?:\.post[0-9]+)?)?")
_NORMALIZED_CODEX_VERSION_PATTERN = re.compile(
r"[0-9]+(?:\.[0-9]+)*(?:(?:a|b|rc)[0-9]+)?(?:\.post[0-9]+)?"
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Resolve a Python runtime package version to its Codex release tag."
)
parser.add_argument("python_version")
parser.add_argument("--github-output", type=Path, required=True)
args = parser.parse_args(argv)
try:
python_version, release_tag = resolve_python_runtime_release(args.python_version)
except RuntimeError as exc:
print(exc, file=sys.stderr)
return 1
with args.github_output.open("a", encoding="utf-8") as output:
print(f"python_version={python_version}", file=output)
print(f"release_tag={release_tag}", file=output)
return 0
def resolve_python_runtime_release(python_version: str) -> tuple[str, str]:
if _PYTHON_RUNTIME_VERSION_PATTERN.fullmatch(python_version) is None:
raise RuntimeError(
"Python runtime version must be stable, a numbered alpha, or an "
"alpha post-release, for example 0.136.0, 0.136.0a2, or "
f"0.136.0a2.post1; found {python_version}"
)
return python_version, codex_release_tag(python_version)
def codex_release_tag(version: str) -> str:
return f"rust-v{codex_release_version(version)}"
def codex_release_version(version: str) -> str:
normalized = normalize_codex_version(version)
alpha_hotfix = re.fullmatch(
r"([0-9]+(?:\.[0-9]+)*)a([0-9]+)\.post([0-9]+)",
normalized,
)
if alpha_hotfix is not None:
base, alpha, hotfix = alpha_hotfix.groups()
return f"{base}-alpha.{alpha}.{hotfix}"
prerelease = re.fullmatch(r"([0-9]+(?:\.[0-9]+)*)(a|b|rc)([0-9]+)", normalized)
if prerelease is None:
return normalized
base, prerelease_kind, number = prerelease.groups()
prerelease_name = {"a": "alpha", "b": "beta", "rc": "rc"}[prerelease_kind]
return f"{base}-{prerelease_name}.{number}"
def normalize_codex_version(version: str) -> str:
normalized = version.strip()
if normalized.startswith("rust-v"):
normalized = normalized.removeprefix("rust-v")
elif normalized.startswith("v"):
normalized = normalized.removeprefix("v")
normalized = re.sub(r"-alpha\.?([0-9]+)\.([0-9]+)$", r"a\1.post\2", normalized)
normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized)
normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized)
normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized)
if _NORMALIZED_CODEX_VERSION_PATTERN.fullmatch(normalized) is None:
raise RuntimeError(f"Could not normalize Codex version {version!r} to a PEP 440 version")
return normalized
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -17,6 +17,12 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Sequence, get_args, get_origin
_SDK_PYTHON_ROOT = str(Path(__file__).resolve().parents[1])
if _SDK_PYTHON_ROOT not in sys.path:
sys.path.insert(0, _SDK_PYTHON_ROOT)
from release_version import normalize_codex_version # noqa: E402
SDK_DISTRIBUTION_NAME = "openai-codex"
RUNTIME_DISTRIBUTION_NAME = "openai-codex-cli-bin"
RUNTIME_PACKAGE_ROOT = Path("src") / "codex_cli_bin"
@@ -130,22 +136,6 @@ def pinned_runtime_codex_path() -> Path:
return codex_path
def normalize_codex_version(version: str) -> str:
normalized = version.strip()
if normalized.startswith("rust-v"):
normalized = normalized.removeprefix("rust-v")
elif normalized.startswith("v"):
normalized = normalized.removeprefix("v")
normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized)
normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized)
normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized)
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*(?:(?:a|b|rc)[0-9]+)?", normalized):
raise RuntimeError(f"Could not normalize Codex version {version!r} to a PEP 440 version")
return normalized
def _copy_package_tree(src: Path, dst: Path) -> None:
if dst.exists():
if dst.is_dir():
@@ -1384,7 +1374,8 @@ def build_parser() -> argparse.ArgumentParser:
required=True,
help=(
"Codex release version to write into the staged runtime package. "
"Accepts PEP 440 versions or release tags such as rust-v0.116.0-alpha.1."
"Accepts PEP 440 versions or release tags such as "
"rust-v0.116.0-alpha.1.2."
),
)
stage_runtime_parser.add_argument(

View File

@@ -3,6 +3,7 @@ import importlib.util
import io
import json
import os
import subprocess
import sys
import tarfile
import urllib.error
@@ -51,6 +52,18 @@ def _load_runtime_setup_module():
return module
def _load_release_version_module():
"""Load the shared release-version conversions used by release tooling."""
script_path = ROOT / "release_version.py"
spec = importlib.util.spec_from_file_location("release_version", script_path)
if spec is None or spec.loader is None:
raise AssertionError(f"Failed to load release-version module: {script_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _write_fake_codex_package(package_dir: Path, script) -> Path:
(package_dir / "bin").mkdir(parents=True)
(package_dir / "codex-resources").mkdir()
@@ -571,13 +584,19 @@ def test_runtime_setup_reads_independent_runtime_pin_and_release_tags() -> None:
"normalized_release_version": runtime_setup._normalized_package_version(
"rust-v0.116.0-alpha.1"
),
"normalized_alpha_hotfix_version": runtime_setup._normalized_package_version(
"rust-v0.116.0-alpha.1.2"
),
"release_tag": runtime_setup._release_tag("0.116.0a1"),
"alpha_hotfix_release_tag": runtime_setup._release_tag("0.116.0a1.post2"),
} == {
"package_name": "openai-codex-cli-bin",
"sdk_template_version": "0.0.0-dev",
"runtime_pin": "0.144.4",
"normalized_release_version": "0.116.0a1",
"normalized_alpha_hotfix_version": "0.116.0a1.post2",
"release_tag": "rust-v0.116.0-alpha.1",
"alpha_hotfix_release_tag": "rust-v0.116.0-alpha.1.2",
}
@@ -703,11 +722,54 @@ def test_normalize_codex_version_accepts_release_tags_and_pep440_versions() -> N
script = _load_update_script_module()
assert script.normalize_codex_version("rust-v0.116.0-alpha.1") == "0.116.0a1"
assert script.normalize_codex_version("rust-v0.116.0-alpha.1.2") == "0.116.0a1.post2"
assert script.normalize_codex_version("v0.116.0-beta.2") == "0.116.0b2"
assert script.normalize_codex_version("0.116.0rc3") == "0.116.0rc3"
assert script.normalize_codex_version("0.116.0") == "0.116.0"
def test_release_version_conversions_map_python_versions_to_codex_tags() -> None:
release_version = _load_release_version_module()
assert {
version: release_version.codex_release_tag(version)
for version in ["0.116.0", "0.116.0a1", "0.116.0a1.post2"]
} == {
"0.116.0": "rust-v0.116.0",
"0.116.0a1": "rust-v0.116.0-alpha.1",
"0.116.0a1.post2": "rust-v0.116.0-alpha.1.2",
}
def test_release_version_cli_writes_python_runtime_outputs(tmp_path: Path) -> None:
github_output = tmp_path / "github-output"
result = subprocess.run(
[
sys.executable,
str(ROOT / "release_version.py"),
"0.116.0a1.post2",
"--github-output",
str(github_output),
],
text=True,
capture_output=True,
check=False,
)
assert {
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"github_output": github_output.read_text(),
} == {
"returncode": 0,
"stdout": "",
"stderr": "",
"github_output": ("python_version=0.116.0a1.post2\nrelease_tag=rust-v0.116.0-alpha.1.2\n"),
}
def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
script = _load_update_script_module()
staging_dir = tmp_path / "runtime-stage"