From 26ce6649a256c00f89121cf56bdefb5c90577319 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 9 Sep 2026 04:58:00 +0000 Subject: [PATCH] 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 --- .github/workflows/python-runtime-build.yml | 5 + .github/workflows/python-runtime-release.yml | 2 +- .github/workflows/python-sdk-build.yml | 60 +++++ .github/workflows/python-sdk-release.yml | 75 ++---- sdk/python/release_version.py | 6 +- sdk/python/scripts/update_sdk_artifacts.py | 32 ++- .../test_artifact_workflow_and_binaries.py | 230 ++++++++++++++++-- 7 files changed, 329 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/python-sdk-build.yml diff --git a/.github/workflows/python-runtime-build.yml b/.github/workflows/python-runtime-build.yml index 433c08614b..338525c90c 100644 --- a/.github/workflows/python-runtime-build.yml +++ b/.github/workflows/python-runtime-build.yml @@ -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 diff --git a/.github/workflows/python-runtime-release.yml b/.github/workflows/python-runtime-release.yml index b5c7ba64a1..f7f0b4af23 100644 --- a/.github/workflows/python-runtime-release.yml +++ b/.github/workflows/python-runtime-release.yml @@ -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" diff --git a/.github/workflows/python-sdk-build.yml b/.github/workflows/python-sdk-build.yml new file mode 100644 index 0000000000..c6aac56464 --- /dev/null +++ b/.github/workflows/python-sdk-build.yml @@ -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 diff --git a/.github/workflows/python-sdk-release.yml b/.github/workflows/python-sdk-release.yml index b7b69ac55d..0e01f23a84 100644 --- a/.github/workflows/python-sdk-release.yml +++ b/.github/workflows/python-sdk-release.yml @@ -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: diff --git a/sdk/python/release_version.py b/sdk/python/release_version.py index 0340753e78..86345550f3 100644 --- a/sdk/python/release_version.py +++ b/sdk/python/release_version.py @@ -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 diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 76537dc900..1b022177d9 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -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( diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index da567cd026..80a5da37ca 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -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