mirror of
https://github.com/openai/codex.git
synced 2026-05-03 10:56:37 +00:00
Publish Python SDK with Codex-pinned versioning (#18996)
**note**: a large chunk of this diff comes from regenerating Python types after app-server schema changes on `main`. This is PR 3 of 3 for the Python SDK PyPI publishing split. PR #18862 refreshed the generated SDK surface, and PR #18865 made the runtime package publishable as `openai-codex-cli-bin`; this final PR makes the SDK package publishable as `openai-codex-app-server-sdk` and pins both packages to the same Codex runtime version. The key idea is that the published SDK version is the Codex runtime version. That one version now drives the SDK package version, the exact runtime dependency, the client version reported by the SDK, and the bootstrap runtime pin. This keeps release-time versioning in one lane instead of scattering checked-in literals through the package. ## What changed - Rename the SDK distribution from `codex-app-server-sdk` to `openai-codex-app-server-sdk` for conflict-free PyPI publishing. - Use `stage-sdk --codex-version ...` with one Codex version for both the SDK package version and exact `openai-codex-cli-bin` dependency. - Preserve hidden legacy `--runtime-version` / `--sdk-version` args only to reject mismatched versions during staging. - Map PEP 440 package versions back to Codex release tags for runtime setup downloads, e.g. `0.116.0a1` -> `rust-v0.116.0-alpha.1`. - Derive `codex_app_server.__version__`, the default `AppServerConfig.client_version`, and `_runtime_setup.pinned_runtime_version()` from the SDK package/project version instead of hardcoding duplicate version strings. - Carry the current generated SDK refresh from `main` so `generate-types` stays clean after recent app-server schema changes. - Update `sdk/python/uv.lock` for the renamed editable package. ## Validation - `uv run --extra dev pytest` in `sdk/python` -> 59 passed, 37 skipped. - Targeted `uv run ruff check` for the touched SDK files. - `git diff --check`. - Staged runtime with `--codex-version rust-v0.116.0-alpha.1 --platform-tag macosx_11_0_arm64`. - Staged SDK with `--codex-version rust-v0.116.0-alpha.1`. - Built runtime wheel, SDK wheel, and SDK sdist. - `twine check /tmp/codex-python-pr3-build/dist/*` -> passed. - Clean venv smoke installed `openai-codex-app-server-sdk==0.116.0a1` from local dist and pulled `openai-codex-cli-bin==0.116.0a1`. - Smoke imports passed for `Codex` and `bundled_codex_path()`.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -16,7 +18,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE_NAME = "openai-codex-cli-bin"
|
||||
PINNED_RUNTIME_VERSION = "0.116.0-alpha.1"
|
||||
SDK_PACKAGE_NAME = "openai-codex-app-server-sdk"
|
||||
REPO_SLUG = "openai/codex"
|
||||
|
||||
|
||||
@@ -25,7 +27,16 @@ class RuntimeSetupError(RuntimeError):
|
||||
|
||||
|
||||
def pinned_runtime_version() -> str:
|
||||
return PINNED_RUNTIME_VERSION
|
||||
source_version = _source_tree_project_version()
|
||||
if source_version is not None:
|
||||
return _normalized_package_version(source_version)
|
||||
|
||||
try:
|
||||
return _normalized_package_version(importlib.metadata.version(SDK_PACKAGE_NAME))
|
||||
except importlib.metadata.PackageNotFoundError as exc:
|
||||
raise RuntimeSetupError(
|
||||
f"Unable to resolve {SDK_PACKAGE_NAME} version for runtime pinning."
|
||||
) from exc
|
||||
|
||||
|
||||
def ensure_runtime_package_installed(
|
||||
@@ -39,7 +50,10 @@ def ensure_runtime_package_installed(
|
||||
installed_version = _installed_runtime_version(python_executable)
|
||||
normalized_requested = _normalized_package_version(requested_version)
|
||||
|
||||
if installed_version is not None and _normalized_package_version(installed_version) == normalized_requested:
|
||||
if (
|
||||
installed_version is not None
|
||||
and _normalized_package_version(installed_version) == normalized_requested
|
||||
):
|
||||
return requested_version
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="codex-python-runtime-") as temp_root_str:
|
||||
@@ -61,7 +75,10 @@ def ensure_runtime_package_installed(
|
||||
importlib.invalidate_caches()
|
||||
|
||||
installed_version = _installed_runtime_version(python_executable)
|
||||
if installed_version is None or _normalized_package_version(installed_version) != normalized_requested:
|
||||
if (
|
||||
installed_version is None
|
||||
or _normalized_package_version(installed_version) != normalized_requested
|
||||
):
|
||||
raise RuntimeSetupError(
|
||||
f"Expected {PACKAGE_NAME} {requested_version} in {python_executable}, "
|
||||
f"but found {installed_version!r} after installation."
|
||||
@@ -121,7 +138,8 @@ def _installed_runtime_version(python_executable: str | Path) -> str | None:
|
||||
|
||||
|
||||
def _release_metadata(version: str) -> dict[str, object]:
|
||||
url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/rust-v{version}"
|
||||
release_tag = _release_tag(version)
|
||||
url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/{release_tag}"
|
||||
token = _github_token()
|
||||
attempts = [True, False] if token is not None else [False]
|
||||
last_error: urllib.error.HTTPError | None = None
|
||||
@@ -146,7 +164,7 @@ def _release_metadata(version: str) -> dict[str, object]:
|
||||
|
||||
assert last_error is not None
|
||||
raise RuntimeSetupError(
|
||||
f"Failed to resolve release metadata for rust-v{version} from {REPO_SLUG}: "
|
||||
f"Failed to resolve release metadata for {release_tag} from {REPO_SLUG}: "
|
||||
f"{last_error.code} {last_error.reason}"
|
||||
) from last_error
|
||||
|
||||
@@ -154,9 +172,10 @@ def _release_metadata(version: str) -> dict[str, object]:
|
||||
def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
asset_name = platform_asset_name()
|
||||
archive_path = temp_root / asset_name
|
||||
release_tag = _release_tag(version)
|
||||
|
||||
browser_download_url = (
|
||||
f"https://github.com/{REPO_SLUG}/releases/download/rust-v{version}/{asset_name}"
|
||||
f"https://github.com/{REPO_SLUG}/releases/download/{release_tag}/{asset_name}"
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
browser_download_url,
|
||||
@@ -172,7 +191,9 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
metadata = _release_metadata(version)
|
||||
assets = metadata.get("assets")
|
||||
if not isinstance(assets, list):
|
||||
raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.")
|
||||
raise RuntimeSetupError(
|
||||
f"Release {release_tag} returned malformed assets metadata."
|
||||
)
|
||||
asset = next(
|
||||
(
|
||||
item
|
||||
@@ -183,7 +204,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
)
|
||||
if asset is None:
|
||||
raise RuntimeSetupError(
|
||||
f"Release rust-v{version} does not contain asset {asset_name} for this platform."
|
||||
f"Release {release_tag} does not contain asset {asset_name} for this platform."
|
||||
)
|
||||
|
||||
api_url = asset.get("url")
|
||||
@@ -198,7 +219,10 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
headers=_github_api_headers("application/octet-stream"),
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh:
|
||||
with (
|
||||
urllib.request.urlopen(request) as response,
|
||||
archive_path.open("wb") as fh,
|
||||
):
|
||||
shutil.copyfileobj(response, fh)
|
||||
return archive_path
|
||||
except urllib.error.HTTPError:
|
||||
@@ -216,7 +240,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
"gh",
|
||||
"release",
|
||||
"download",
|
||||
f"rust-v{version}",
|
||||
release_tag,
|
||||
"--repo",
|
||||
REPO_SLUG,
|
||||
"--pattern",
|
||||
@@ -230,7 +254,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeSetupError(
|
||||
f"gh release download failed for rust-v{version} asset {asset_name}.\n"
|
||||
f"gh release download failed for {release_tag} asset {asset_name}.\n"
|
||||
f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}"
|
||||
) from exc
|
||||
return archive_path
|
||||
@@ -249,7 +273,9 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
|
||||
with zipfile.ZipFile(archive_path) as zip_file:
|
||||
zip_file.extractall(extract_dir)
|
||||
else:
|
||||
raise RuntimeSetupError(f"Unsupported release archive format: {archive_path.name}")
|
||||
raise RuntimeSetupError(
|
||||
f"Unsupported release archive format: {archive_path.name}"
|
||||
)
|
||||
|
||||
binary_name = runtime_binary_name()
|
||||
archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip")
|
||||
@@ -346,12 +372,50 @@ def _github_token() -> str | None:
|
||||
|
||||
|
||||
def _normalized_package_version(version: str) -> str:
|
||||
return version.strip().replace("-alpha.", "a").replace("-beta.", "b")
|
||||
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_project_version() -> str | None:
|
||||
pyproject_path = Path(__file__).resolve().parent / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return None
|
||||
|
||||
match = re.search(
|
||||
r'(?m)^version = "([^"]+)"$',
|
||||
pyproject_path.read_text(encoding="utf-8"),
|
||||
)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PACKAGE_NAME",
|
||||
"PINNED_RUNTIME_VERSION",
|
||||
"SDK_PACKAGE_NAME",
|
||||
"RuntimeSetupError",
|
||||
"ensure_runtime_package_installed",
|
||||
"pinned_runtime_version",
|
||||
|
||||
Reference in New Issue
Block a user