mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
25
.github/workflows/python-runtime-build.yml
vendored
25
.github/workflows/python-runtime-build.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runtime_version:
|
||||
description: "Runtime version to build, for example 0.136.0 or 0.136.0a2."
|
||||
description: "Runtime version to build, for example 0.136.0, 0.136.0a2, or 0.136.0a2.post1."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -29,26 +29,9 @@ jobs:
|
||||
REQUESTED_RUNTIME_VERSION: ${{ inputs.runtime_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
python_version = os.environ["REQUESTED_RUNTIME_VERSION"]
|
||||
if match := re.fullmatch(r"([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)", python_version):
|
||||
release_version = f"{match.group(1)}-alpha.{match.group(2)}"
|
||||
elif re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", python_version):
|
||||
release_version = python_version
|
||||
else:
|
||||
raise SystemExit(
|
||||
"Python runtime version must be stable or a numbered alpha, "
|
||||
f"for example 0.136.0 or 0.136.0a2; found {python_version}"
|
||||
)
|
||||
|
||||
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
|
||||
print(f"python_version={python_version}", file=output)
|
||||
print(f"release_tag=rust-v{release_version}", file=output)
|
||||
PY
|
||||
python3 sdk/python/release_version.py \
|
||||
"$REQUESTED_RUNTIME_VERSION" \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download Python runtime release artifacts
|
||||
env:
|
||||
|
||||
2
.github/workflows/python-runtime-release.yml
vendored
2
.github/workflows/python-runtime-release.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runtime_version:
|
||||
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0 or 0.136.0a2."
|
||||
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0, 0.136.0a2, or 0.136.0a2.post1."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
||||
6
.github/workflows/rust-release.yml
vendored
6
.github/workflows/rust-release.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|| { echo "❌ Not a tag ref"; exit 1; }
|
||||
|
||||
# Release tags must match the version in Cargo.toml.
|
||||
[[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \
|
||||
[[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha(\.[0-9]+){0,2}|beta(\.[0-9]+)?))?$ ]] \
|
||||
|| { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; }
|
||||
|
||||
tag_ver="${GITHUB_REF_NAME#rust-v}"
|
||||
@@ -1220,7 +1220,7 @@ jobs:
|
||||
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
echo "npm_tag=" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
|
||||
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
echo "npm_tag=alpha" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
@@ -1303,7 +1303,7 @@ jobs:
|
||||
# July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/
|
||||
# npm docs: https://docs.npmjs.com/trusted-publishers
|
||||
publish-npm:
|
||||
# Publish to npm for stable releases and alpha pre-releases with numeric suffixes.
|
||||
# Publish to npm for stable releases, numbered alphas, and alpha hotfixes.
|
||||
if: >-
|
||||
${{
|
||||
!cancelled() &&
|
||||
|
||||
@@ -71,8 +71,8 @@ function Assert-ValidReleaseVersion {
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+)?)?$") {
|
||||
throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]."
|
||||
if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-alpha(?:\.[0-9]+){0,2}|-beta(?:\.[0-9]+)?)?$") {
|
||||
throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ validate_version() {
|
||||
return
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$'; then
|
||||
echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]." >&2
|
||||
if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-alpha(\.[0-9]+){0,2}|-beta(\.[0-9]+)?)?$'; then
|
||||
echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -48,6 +48,22 @@ class InstallShTest(unittest.TestCase):
|
||||
)
|
||||
self.assertIn(f"Resolved version: {VERSION}", result.stdout)
|
||||
|
||||
def test_alpha_hotfix_release_is_valid(self) -> None:
|
||||
version = "0.145.0-alpha.23.1"
|
||||
result, requests = run_installer(version)
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(
|
||||
requests,
|
||||
[
|
||||
"https://api.github.com/repos/openai/codex/releases/tags/"
|
||||
f"rust-v{version}",
|
||||
"https://github.com/openai/codex/releases/download/"
|
||||
f"rust-v{version}/codex-package_SHA256SUMS",
|
||||
],
|
||||
)
|
||||
self.assertIn(f"Resolved version: {version}", result.stdout)
|
||||
|
||||
def test_latest_release_reuses_version_metadata(self) -> None:
|
||||
result, requests = run_installer("latest")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
86
sdk/python/release_version.py
Normal file
86
sdk/python/release_version.py
Normal 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())
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user