mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
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:
@@ -7,6 +7,9 @@ on:
|
||||
description: "Runtime version to build, for example 0.136.0, 0.136.0a2, or 0.136.0a2.post1."
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
python_version:
|
||||
value: ${{ jobs.build-python-runtime.outputs.python_version }}
|
||||
|
||||
jobs:
|
||||
build-python-runtime:
|
||||
@@ -15,6 +18,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
python_version: ${{ steps.python_runtime.outputs.python_version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Verify Python runtime wheels are available on PyPI
|
||||
env:
|
||||
PYTHON_RUNTIME_VERSION: ${{ inputs.runtime_version }}
|
||||
PYTHON_RUNTIME_VERSION: ${{ needs.prepare-python-runtime.outputs.python_version }}
|
||||
run: |
|
||||
uv run --no-project --with packaging==26.2 python .github/scripts/verify_pypi_release.py \
|
||||
openai-codex-cli-bin "$PYTHON_RUNTIME_VERSION"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: python-sdk-build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
sdk_version:
|
||||
description: "Python SDK version to build."
|
||||
required: true
|
||||
type: string
|
||||
runtime_version:
|
||||
description: "Exact Python CLI runtime dependency for this SDK release."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-python-sdk:
|
||||
if: github.repository == 'openai/codex'
|
||||
name: build-python-sdk
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build Python SDK package
|
||||
shell: bash
|
||||
env:
|
||||
SDK_VERSION: ${{ inputs.sdk_version }}
|
||||
RUNTIME_VERSION: ${{ inputs.runtime_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -m venv "${RUNNER_TEMP}/python-sdk-build-venv"
|
||||
build_python="${RUNNER_TEMP}/python-sdk-build-venv/bin/python"
|
||||
"$build_python" -m pip install build twine packaging==26.2
|
||||
"$build_python" sdk/python/scripts/update_sdk_artifacts.py \
|
||||
stage-sdk "${RUNNER_TEMP}/openai-codex" \
|
||||
--sdk-version "$SDK_VERSION" \
|
||||
--codex-version "$RUNTIME_VERSION"
|
||||
"$build_python" -m build \
|
||||
--wheel \
|
||||
--sdist \
|
||||
--outdir dist/python-sdk \
|
||||
"${RUNNER_TEMP}/openai-codex"
|
||||
"$build_python" -m twine check --strict dist/python-sdk/*
|
||||
|
||||
- name: Upload Python SDK package
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: python-sdk-package
|
||||
path: dist/python-sdk/*
|
||||
if-no-files-found: error
|
||||
@@ -75,14 +75,25 @@ jobs:
|
||||
with:
|
||||
runtime_version: ${{ needs.resolve-python-release.outputs.runtime_version }}
|
||||
|
||||
# Always publish the exact pinned runtime from this top-level workflow before
|
||||
# building the SDK package. PyPI does not support reusable workflows as
|
||||
# Trusted Publishers.
|
||||
build-python-sdk:
|
||||
name: build-python-sdk
|
||||
needs: resolve-python-release
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/python-sdk-build.yml
|
||||
with:
|
||||
sdk_version: ${{ needs.resolve-python-release.outputs.sdk_version }}
|
||||
runtime_version: ${{ needs.resolve-python-release.outputs.runtime_version }}
|
||||
|
||||
# Publish from the top-level workflow: PyPI does not support reusable
|
||||
# workflows as Trusted Publishers. The runtime must be available before
|
||||
# publishing the SDK that depends on it.
|
||||
publish-python-runtime:
|
||||
if: github.repository == 'openai/codex'
|
||||
name: publish-python-runtime
|
||||
needs:
|
||||
- prepare-python-runtime
|
||||
- build-python-sdk
|
||||
- resolve-python-release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
@@ -120,64 +131,6 @@ jobs:
|
||||
uv run --no-project --with packaging==26.2 python .github/scripts/verify_pypi_release.py \
|
||||
openai-codex-cli-bin "$PYTHON_RUNTIME_VERSION"
|
||||
|
||||
build-python-sdk:
|
||||
if: github.repository == 'openai/codex'
|
||||
name: build-python-sdk
|
||||
needs:
|
||||
- publish-python-runtime
|
||||
- resolve-python-release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build Python SDK package
|
||||
shell: bash
|
||||
env:
|
||||
SDK_VERSION: ${{ needs.resolve-python-release.outputs.sdk_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Build in a glibc Linux image so release type generation installs
|
||||
# the pinned manylinux runtime wheel.
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-e HOME=/tmp/codex-python-sdk-home \
|
||||
-e UV_LINK_MODE=copy \
|
||||
-e SDK_VERSION \
|
||||
-e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \
|
||||
-e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \
|
||||
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
|
||||
-v "${RUNNER_TEMP}:${RUNNER_TEMP}" \
|
||||
-w "${GITHUB_WORKSPACE}/sdk/python" \
|
||||
python:3.12-slim \
|
||||
sh -euxc '
|
||||
python -m venv /tmp/release-tools
|
||||
/tmp/release-tools/bin/python -m pip install build twine uv==0.11.3
|
||||
/tmp/release-tools/bin/uv sync --group dev --frozen
|
||||
/tmp/release-tools/bin/uv run --frozen --no-sync python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk "${SDK_STAGE_DIR}" \
|
||||
--sdk-version "${SDK_VERSION}"
|
||||
/tmp/release-tools/bin/python -m build \
|
||||
--wheel \
|
||||
--sdist \
|
||||
--outdir "${SDK_DIST_DIR}" \
|
||||
"${SDK_STAGE_DIR}"
|
||||
/tmp/release-tools/bin/python -m twine check --strict "${SDK_DIST_DIR}/"*
|
||||
'
|
||||
|
||||
- name: Upload Python SDK package
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: python-sdk-package
|
||||
path: dist/python-sdk/*
|
||||
if-no-files-found: error
|
||||
|
||||
publish-python-sdk:
|
||||
name: publish-python-sdk
|
||||
needs:
|
||||
|
||||
@@ -14,14 +14,16 @@ _NORMALIZED_CODEX_VERSION_PATTERN = re.compile(
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve a Python runtime package version to its Codex release tag."
|
||||
description="Resolve a Python runtime version or Codex release tag to both versions."
|
||||
)
|
||||
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)
|
||||
python_version, release_tag = resolve_python_runtime_release(
|
||||
normalize_codex_version(args.python_version)
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -143,7 +143,11 @@ def _rewrite_project_name(pyproject_text: str, name: str) -> str:
|
||||
return updated
|
||||
|
||||
|
||||
def stage_python_sdk_package(staging_dir: Path, sdk_version: str) -> Path:
|
||||
def stage_python_sdk_package(
|
||||
staging_dir: Path,
|
||||
sdk_version: str,
|
||||
codex_version: str | None = None,
|
||||
) -> Path:
|
||||
package_version = normalize_codex_version(sdk_version)
|
||||
_copy_package_tree(sdk_root(), staging_dir)
|
||||
sdk_bin_dir = staging_dir / "src" / "openai_codex" / "bin"
|
||||
@@ -154,6 +158,23 @@ def stage_python_sdk_package(staging_dir: Path, sdk_version: str) -> Path:
|
||||
pyproject_text = pyproject_path.read_text()
|
||||
pyproject_text = _rewrite_project_name(pyproject_text, SDK_DISTRIBUTION_NAME)
|
||||
pyproject_text = _rewrite_project_version(pyproject_text, package_version)
|
||||
if codex_version is not None:
|
||||
runtime_version = normalize_codex_version(codex_version)
|
||||
pyproject_text, count = re.subn(
|
||||
rf'"{re.escape(RUNTIME_DISTRIBUTION_NAME)}==[^"]+"',
|
||||
f'"{RUNTIME_DISTRIBUTION_NAME}=={runtime_version}"',
|
||||
pyproject_text,
|
||||
)
|
||||
if count != 1:
|
||||
raise RuntimeError(
|
||||
f"Expected exactly one {RUNTIME_DISTRIBUTION_NAME} dependency pin "
|
||||
"in sdk/python/pyproject.toml"
|
||||
)
|
||||
runtime_versions = re.findall(
|
||||
rf'"{re.escape(RUNTIME_DISTRIBUTION_NAME)}==([^"]+)"', pyproject_text
|
||||
)
|
||||
if len(runtime_versions) != 1:
|
||||
raise RuntimeError("Expected exactly one pinned Codex runtime dependency")
|
||||
pyproject_path.write_text(pyproject_text)
|
||||
return staging_dir
|
||||
|
||||
@@ -894,7 +915,7 @@ class PublicFieldSpec:
|
||||
@dataclass(frozen=True)
|
||||
class CliOps:
|
||||
generate_types: Callable[[Path], None]
|
||||
stage_python_sdk_package: Callable[[Path, str], Path]
|
||||
stage_python_sdk_package: Callable[[Path, str, str | None], Path]
|
||||
stage_python_runtime_package: Callable[[Path, str, Path, str | None], Path]
|
||||
|
||||
|
||||
@@ -1323,7 +1344,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
stage_sdk_parser = subparsers.add_parser(
|
||||
"stage-sdk",
|
||||
help="Stage a releasable SDK package from its reviewed generated files and runtime pin",
|
||||
help="Stage a releasable SDK package from the checked-in generated code",
|
||||
)
|
||||
stage_sdk_parser.add_argument(
|
||||
"staging_dir",
|
||||
@@ -1338,6 +1359,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"Accepts PEP 440 versions such as 0.144.4."
|
||||
),
|
||||
)
|
||||
stage_sdk_parser.add_argument(
|
||||
"--codex-version",
|
||||
help="CLI release version to pin; defaults to the checked-in runtime dependency.",
|
||||
)
|
||||
|
||||
stage_runtime_parser = subparsers.add_parser(
|
||||
"stage-runtime",
|
||||
@@ -1400,6 +1425,7 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
|
||||
ops.stage_python_sdk_package(
|
||||
args.staging_dir,
|
||||
normalize_codex_version(args.sdk_version),
|
||||
normalize_codex_version(args.codex_version) if args.codex_version is not None else None,
|
||||
)
|
||||
elif args.command == "stage-runtime":
|
||||
ops.stage_python_runtime_package(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user