Build Python SDK artifacts before publishing the runtime (#44061)

## Why

The SDK release workflow published the runtime before building the SDK, so an SDK build failure could leave the runtime published on its own.

## What changed

- Extract a reusable SDK build workflow that packages checked-in generated code and runs alongside runtime preparation. Require both builds before publishing the runtime, and verify runtime availability before publishing the SDK.
- Add `stage-sdk --codex-version` to set an explicit runtime dependency independently of the SDK version, retaining the checked-in pin by default and rejecting missing or duplicate pins.
- Accept Codex release tags in the runtime version resolver and use its normalized Python version for standalone runtime PyPI verification.

## Testing

Add coverage for wheel and source distribution metadata, preservation of checked-in code, independent beta SDK versions, runtime tag normalization, and invalid versions or dependency pins.

GitOrigin-RevId: feb572fadcf5d148814b26b480b4bae5ef6a39c5
This commit is contained in:
Ahmed Ibrahim
2026-09-09 05:14:27 +00:00
committed by copyberry
parent 3d3df0a0ca
commit 26ce6649a2
7 changed files with 329 additions and 81 deletions
@@ -3,10 +3,13 @@ import importlib.util
import io
import json
import os
import re
import subprocess
import sys
import tarfile
import urllib.error
import zipfile
from email.parser import BytesParser
from pathlib import Path
import pytest
@@ -783,14 +786,28 @@ def test_release_version_conversions_map_python_versions_to_codex_tags() -> None
}
def test_release_version_cli_writes_python_runtime_outputs(tmp_path: Path) -> None:
@pytest.mark.parametrize(
("version", "python_version", "release_tag"),
[
("0.116.0a1.post2", "0.116.0a1.post2", "rust-v0.116.0-alpha.1.2"),
("rust-v1.2.3", "1.2.3", "rust-v1.2.3"),
("rust-v1.2.3-alpha.4", "1.2.3a4", "rust-v1.2.3-alpha.4"),
("rust-v1.2.3-alpha.4.5", "1.2.3a4.post5", "rust-v1.2.3-alpha.4.5"),
],
)
def test_release_version_cli_writes_python_runtime_outputs(
tmp_path: Path,
version: str,
python_version: str,
release_tag: str,
) -> None:
github_output = tmp_path / "github-output"
result = subprocess.run(
[
sys.executable,
str(ROOT / "release_version.py"),
"0.116.0a1.post2",
version,
"--github-output",
str(github_output),
],
@@ -808,7 +825,7 @@ def test_release_version_cli_writes_python_runtime_outputs(tmp_path: Path) -> No
"returncode": 0,
"stdout": "",
"stderr": "",
"github_output": ("python_version=0.116.0a1.post2\nrelease_tag=rust-v0.116.0-alpha.1.2\n"),
"github_output": f"python_version={python_version}\nrelease_tag={release_tag}\n",
}
@@ -879,10 +896,27 @@ def test_runtime_package_layout_is_included_by_wheel_config(
]
def test_stage_sdk_release_packages_reviewed_artifacts(tmp_path: Path) -> None:
@pytest.fixture
def sdk_release_source(tmp_path: Path) -> Path:
script = _load_update_script_module()
source = tmp_path / "sdk-source"
script._copy_package_tree(ROOT, source)
project = source / "pyproject.toml"
project.write_text(
re.sub(
r"openai-codex-cli-bin==[^\"\s]+", "openai-codex-cli-bin==0.153.0", project.read_text()
)
)
return source
def test_stage_sdk_release_packages_reviewed_artifacts(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
staged = tmp_path / "sdk-stage"
source_project = tomllib.loads((ROOT / "pyproject.toml").read_text())
source_project = tomllib.loads((sdk_release_source / "pyproject.toml").read_text())
generated_paths = [
"src/openai_codex/generated/v2_all.py",
"src/openai_codex/generated/notification_registry.py",
@@ -921,30 +955,94 @@ def test_stage_sdk_release_packages_reviewed_artifacts(tmp_path: Path) -> None:
assert not any((staged / "src" / "openai_codex").glob("bin/**"))
def test_stage_sdk_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
@pytest.mark.parametrize("sdk_version", ["0.154.0", "0.2.0b1"])
def test_built_sdk_uses_explicit_release_versions(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path, sdk_version: str
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
source_project = (sdk_release_source / "pyproject.toml").read_bytes()
expected_dependencies = {
*tomllib.loads(source_project.decode())["project"]["dependencies"],
"openai-codex-cli-bin==0.154.0",
} - {"openai-codex-cli-bin==0.153.0"}
reviewed_files = {
path: (sdk_release_source / path).read_bytes()
for path in (
"src/openai_codex/generated/v2_all.py",
"src/openai_codex/generated/notification_registry.py",
"src/openai_codex/api.py",
)
}
staged = script.stage_python_sdk_package(tmp_path / "sdk-stage", sdk_version, "rust-v0.154.0")
dist = tmp_path / "dist"
subprocess.run(
["uv", "build", "--wheel", "--sdist", "--out-dir", str(dist), str(staged)],
check=True,
capture_output=True,
text=True,
)
with zipfile.ZipFile(next(dist.glob("*.whl"))) as wheel:
metadata = [
wheel.read(next(name for name in wheel.namelist() if name.endswith("/METADATA")))
]
assert {
path: wheel.read(path.removeprefix("src/")) for path in reviewed_files
} == reviewed_files
with tarfile.open(next(dist.glob("*.tar.gz"))) as sdist:
prefix = f"openai_codex-{sdk_version}/"
metadata.append(sdist.extractfile(prefix + "PKG-INFO").read())
assert {
path: sdist.extractfile(prefix + path).read() for path in reviewed_files
} == reviewed_files
for content in metadata:
package = BytesParser().parsebytes(content)
assert {
"name": package["Name"],
"version": package["Version"],
"dependencies": set(package.get_all("Requires-Dist")),
} == {
"name": "openai-codex",
"version": sdk_version,
"dependencies": expected_dependencies,
}
assert (sdk_release_source / "pyproject.toml").read_bytes() == source_project
assert {
path: (sdk_release_source / path).read_bytes() for path in reviewed_files
} == reviewed_files
def test_stage_sdk_release_replaces_existing_staging_dir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
staging_dir = tmp_path / "sdk-stage"
old_file = staging_dir / "stale.txt"
old_file.parent.mkdir(parents=True)
old_file.write_text("stale")
staged = script.stage_python_sdk_package(staging_dir, "0.147.0")
staged = script.stage_python_sdk_package(staging_dir, "0.153.0")
assert staged == staging_dir
assert not old_file.exists()
def test_sdk_release_matches_stable_runtime(tmp_path: Path) -> None:
def test_sdk_release_matches_stable_runtime(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
package_archive = _write_fake_codex_package_archive(tmp_path, script)
sdk_stage = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
"0.147.0",
"0.153.0",
)
runtime_stage = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"0.147.0",
"0.153.0",
package_archive,
)
@@ -956,11 +1054,11 @@ def test_sdk_release_matches_stable_runtime(tmp_path: Path) -> None:
"runtime_version": runtime_pyproject["project"]["version"],
"sdk_dependencies": sdk_pyproject["project"]["dependencies"],
} == {
"sdk_version": "0.147.0",
"runtime_version": "0.147.0",
"sdk_version": "0.153.0",
"runtime_version": "0.153.0",
"sdk_dependencies": [
"pydantic>=2.12",
"openai-codex-cli-bin==0.147.0",
"openai-codex-cli-bin==0.153.0",
],
}
@@ -984,7 +1082,9 @@ def test_stage_runtime_stages_package_without_type_generation(tmp_path: Path) ->
def fake_generate_types(_schema_dir: Path) -> None:
calls.append("generate_types")
def fake_stage_sdk_package(_staging_dir: Path, _codex_version: str) -> Path:
def fake_stage_sdk_package(
_staging_dir: Path, _sdk_version: str, _codex_version: str | None
) -> Path:
raise AssertionError("sdk staging should not run for stage-runtime")
def fake_stage_runtime_package(
@@ -1099,3 +1199,105 @@ def test_broken_runtime_package_does_not_fall_back() -> None:
client_module.resolve_codex_bin(client_module.CodexConfig(), ops)
assert str(exc_info.value) == ("missing packaged binary")
@pytest.mark.parametrize("version", ["rust-v1.2.3-alpha", "rust-v1.2.3-beta.1", "invalid"])
def test_release_version_cli_rejects_unsupported_runtime_releases(
tmp_path: Path, version: str
) -> None:
github_output = tmp_path / "github-output"
result = subprocess.run(
[
sys.executable,
str(ROOT / "release_version.py"),
version,
"--github-output",
str(github_output),
],
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 1
assert not github_output.exists()
@pytest.mark.parametrize("runtime_dependency", ["", ', "openai-codex-cli-bin==1.2.3"' * 2])
def test_stage_sdk_release_rejects_missing_or_duplicate_runtime_pin(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
runtime_dependency: str,
) -> None:
script = _load_update_script_module()
template = tmp_path / "template"
template.mkdir()
(template / "pyproject.toml").write_text(
'[project]\nname = "openai-codex"\nversion = "0.0.0"\n'
f'dependencies = ["pydantic>=2.12"{runtime_dependency}]\n'
)
monkeypatch.setattr(script, "sdk_root", lambda: template)
with pytest.raises(RuntimeError, match="Expected exactly one openai-codex-cli-bin"):
script.stage_python_sdk_package(tmp_path / "sdk-stage", "1.2.3", "1.2.3")
def test_stage_sdk_rejects_empty_runtime_version(tmp_path: Path) -> None:
script = _load_update_script_module()
args = script.parse_args(
["stage-sdk", str(tmp_path / "sdk-stage"), "--sdk-version", "1.2.3", "--codex-version", ""]
)
with pytest.raises(RuntimeError, match="Could not normalize Codex version"):
script.run_command(args, script.default_cli_ops())
def test_sdk_beta_can_pin_an_independent_runtime(tmp_path: Path) -> None:
script = _load_update_script_module()
staged = script.stage_python_sdk_package(tmp_path / "sdk-beta", "0.1.0b1", "0.153.0")
project = tomllib.loads((staged / "pyproject.toml").read_text())["project"]
assert (project["version"], project["dependencies"]) == (
"0.1.0b1",
["pydantic>=2.12", "openai-codex-cli-bin==0.153.0"],
)
@pytest.mark.parametrize(
("release_tag", "package_version"),
[
("rust-v1.2.3", "1.2.3"),
("rust-v1.2.3-alpha.4", "1.2.3a4"),
("rust-v1.2.3-alpha.4.5", "1.2.3a4.post5"),
],
)
def test_sdk_release_matches_runtime(
tmp_path: Path, release_tag: str, package_version: str
) -> None:
script = _load_update_script_module()
package_archive = _write_fake_codex_package_archive(tmp_path, script)
source_pyproject = (script.sdk_root() / "pyproject.toml").read_text()
sdk_stage = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
release_tag,
release_tag,
)
runtime_stage = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
release_tag,
package_archive,
)
sdk_pyproject = tomllib.loads((sdk_stage / "pyproject.toml").read_text())
runtime_pyproject = tomllib.loads((runtime_stage / "pyproject.toml").read_text())
assert {
"sdk_version": sdk_pyproject["project"]["version"],
"runtime_version": runtime_pyproject["project"]["version"],
"sdk_dependencies": sdk_pyproject["project"]["dependencies"],
} == {
"sdk_version": package_version,
"runtime_version": package_version,
"sdk_dependencies": [
"pydantic>=2.12",
f"openai-codex-cli-bin=={package_version}",
],
}
assert (script.sdk_root() / "pyproject.toml").read_text() == source_pyproject