Gate Python SDK publishing on runtime availability and verify PyPI files (#44055)

## Why

The SDK publish job needs to wait for runtime wheels to become available on PyPI, and release reruns need to tolerate SDK files that have already been uploaded.

## What changed

- Require runtime publication and verification before publishing the SDK, and enable `skip-existing` for SDK uploads.
- Share a PyPI verifier between runtime and SDK releases. Require the exact expected artifact set, including the SDK wheel and source distribution.
- Canonicalize versions with `packaging.version.Version` and retry registry errors, malformed responses, and incomplete artifact sets with a bounded retry loop.

## Testing

Add unit tests for transient failures and malformed responses, waiting for complete SDK artifacts, canonical version lookup, and retry exhaustion when runtime wheels are missing. Run them in repository checks.

GitOrigin-RevId: 9cb2ddced4ce6d509ebfd130511ab3f39239dd62
This commit is contained in:
Ahmed Ibrahim
2026-09-09 04:39:58 +00:00
committed by copyberry
parent c55db1b8d9
commit 96c2b4377f
5 changed files with 224 additions and 103 deletions

View File

@@ -0,0 +1,96 @@
import http.client
import io
import json
import unittest
import urllib.error
from unittest.mock import MagicMock, patch
from verify_pypi_release import verify_release
class VerifyPyPIReleaseTest(unittest.TestCase):
def test_waits_for_complete_release_after_registry_error(self) -> None:
wheel = "openai_codex-1.2.3-py3-none-any.whl"
sdist = "openai_codex-1.2.3.tar.gz"
reset_response = MagicMock()
reset_response.__enter__.return_value.read.side_effect = ConnectionResetError(
"connection reset while reading response"
)
responses = [
urllib.error.URLError("registry unavailable"),
TimeoutError("registry timed out"),
http.client.IncompleteRead(b"{", 20),
reset_response,
io.StringIO('{"urls":'),
*[
io.StringIO(json.dumps(data))
for data in (
None,
[],
{},
{"urls": None},
{"urls": {}},
{"urls": [None]},
{"urls": [{}]},
{"urls": [{"filename": 42}]},
)
],
io.StringIO(json.dumps({"urls": [{"filename": wheel}]})),
io.StringIO(
json.dumps({"urls": [{"filename": wheel}, {"filename": sdist}]})
),
]
with (
patch(
"verify_pypi_release.urllib.request.urlopen", side_effect=responses
) as urlopen,
patch("verify_pypi_release.time.sleep") as sleep,
):
verify_release("openai-codex", "1.2.3")
self.assertEqual(urlopen.call_count, 15)
self.assertEqual(sleep.call_count, 14)
urlopen.assert_called_with(
"https://pypi.org/pypi/openai-codex/1.2.3/json", timeout=30
)
def test_uses_canonical_version_for_lookup_and_artifact_names(self) -> None:
data = {
"urls": [
{"filename": name}
for name in (
"openai_codex-1.2.3b1-py3-none-any.whl",
"openai_codex-1.2.3b1.tar.gz",
)
]
}
with (
patch(
"verify_pypi_release.urllib.request.urlopen",
return_value=io.StringIO(json.dumps(data)),
) as urlopen,
patch("verify_pypi_release.time.sleep") as sleep,
):
verify_release("openai-codex", "1.2.3b01")
urlopen.assert_called_once_with(
"https://pypi.org/pypi/openai-codex/1.2.3b1/json", timeout=30
)
sleep.assert_not_called()
def test_fails_after_bounded_retries_when_runtime_wheels_are_missing(self) -> None:
with (
patch(
"verify_pypi_release.urllib.request.urlopen",
side_effect=lambda *args, **kwargs: io.StringIO('{"urls": []}'),
) as urlopen,
patch("verify_pypi_release.time.sleep") as sleep,
self.assertRaisesRegex(SystemExit, "did not become available on PyPI"),
):
verify_release("openai-codex-cli-bin", "1.2.3a4.post5")
self.assertEqual(urlopen.call_count, 30)
self.assertEqual(sleep.call_count, 29)
if __name__ == "__main__":
unittest.main()

76
.github/scripts/verify_pypi_release.py vendored Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Wait for every Python release artifact to become visible on PyPI."""
import argparse
import http.client
import json
import time
import urllib.request
from packaging.version import Version
RUNTIME_PLATFORM_TAGS = {
"macosx_10_9_x86_64",
"macosx_11_0_arm64",
"manylinux_2_17_aarch64",
"manylinux_2_17_x86_64",
"musllinux_1_1_aarch64",
"musllinux_1_1_x86_64",
"win_amd64",
"win_arm64",
}
def verify_release(package: str, version: str) -> None:
version = str(Version(version))
name = package.replace("-", "_")
if package == "openai-codex-cli-bin":
expected = {
f"{name}-{version}-py3-none-{tag}.whl" for tag in RUNTIME_PLATFORM_TAGS
}
else:
expected = {f"{name}-{version}-py3-none-any.whl", f"{name}-{version}.tar.gz"}
for attempt in range(30):
try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/{package}/{version}/json", timeout=30
) as response:
data = json.load(response)
if not isinstance(data, dict) or not isinstance(data.get("urls"), list):
raise ValueError("Expected a PyPI response with a urls list")
if any(
not isinstance(file, dict)
or not isinstance(file.get("filename"), str)
for file in data["urls"]
):
raise ValueError("Expected each PyPI file to have a filename")
actual = {file["filename"] for file in data["urls"]}
except (
OSError,
http.client.HTTPException,
ValueError,
) as error:
print(f"Could not read {package} {version} from PyPI: {error}.")
else:
if actual == expected:
print(f"All {package} {version} files are available on PyPI.")
return
print(f"Missing files: {sorted(expected - actual)}")
print(f"Unexpected files: {sorted(actual - expected)}")
if attempt < 29:
time.sleep(10)
raise SystemExit(f"{package} {version} files did not become available on PyPI.")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", choices=["openai-codex-cli-bin", "openai-codex"])
parser.add_argument("version")
args = parser.parse_args()
verify_release(args.package, args.version)
if __name__ == "__main__":
main()

View File

@@ -33,6 +33,11 @@ jobs:
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download Python runtime wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -45,56 +50,14 @@ jobs:
packages-dir: dist/python-runtime
skip-existing: true
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Verify Python runtime wheels are available on PyPI
env:
PYTHON_RUNTIME_VERSION: ${{ inputs.runtime_version }}
run: |
set -euo pipefail
for attempt in {1..30}; do
if python3 - <<'PY'
import json
import os
import urllib.error
import urllib.request
version = os.environ["PYTHON_RUNTIME_VERSION"]
tags = {
"macosx_10_9_x86_64",
"macosx_11_0_arm64",
"manylinux_2_17_aarch64",
"manylinux_2_17_x86_64",
"musllinux_1_1_aarch64",
"musllinux_1_1_x86_64",
"win_amd64",
"win_arm64",
}
expected = {
f"openai_codex_cli_bin-{version}-py3-none-{tag}.whl"
for tag in tags
}
try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/openai-codex-cli-bin/{version}/json",
timeout=30,
) as response:
payload = json.load(response)
except urllib.error.URLError as error:
print(f"Could not read runtime {version} from PyPI: {error}.")
raise SystemExit(1) from error
actual = {file["filename"] for file in payload["urls"]}
if actual != expected:
print(f"Missing runtime wheels: {sorted(expected - actual)}")
print(f"Unexpected runtime files: {sorted(actual - expected)}")
raise SystemExit(1)
PY
then
exit 0
fi
echo "Runtime wheels are not available on PyPI yet; retrying (${attempt}/30)."
sleep 10
done
echo "Runtime wheels did not become available on PyPI."
exit 1
uv run --no-project --with packaging==26.2 python .github/scripts/verify_pypi_release.py \
openai-codex-cli-bin "$PYTHON_RUNTIME_VERSION"

View File

@@ -91,6 +91,11 @@ jobs:
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download Python runtime wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -103,59 +108,17 @@ jobs:
packages-dir: dist/python-runtime
skip-existing: true
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Verify Python runtime wheels are available on PyPI
env:
PYTHON_RUNTIME_VERSION: ${{ needs.resolve-python-release.outputs.runtime_version }}
run: |
set -euo pipefail
for attempt in {1..30}; do
if python3 - <<'PY'
import json
import os
import urllib.error
import urllib.request
version = os.environ["PYTHON_RUNTIME_VERSION"]
tags = {
"macosx_10_9_x86_64",
"macosx_11_0_arm64",
"manylinux_2_17_aarch64",
"manylinux_2_17_x86_64",
"musllinux_1_1_aarch64",
"musllinux_1_1_x86_64",
"win_amd64",
"win_arm64",
}
expected = {
f"openai_codex_cli_bin-{version}-py3-none-{tag}.whl"
for tag in tags
}
try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/openai-codex-cli-bin/{version}/json",
timeout=30,
) as response:
payload = json.load(response)
except urllib.error.URLError as error:
print(f"Could not read runtime {version} from PyPI: {error}.")
raise SystemExit(1) from error
actual = {file["filename"] for file in payload["urls"]}
if actual != expected:
print(f"Missing runtime wheels: {sorted(expected - actual)}")
print(f"Unexpected runtime files: {sorted(actual - expected)}")
raise SystemExit(1)
PY
then
exit 0
fi
echo "Runtime wheels are not available on PyPI yet; retrying (${attempt}/30)."
sleep 10
done
echo "Runtime wheels did not become available on PyPI."
exit 1
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'
@@ -217,7 +180,10 @@ jobs:
publish-python-sdk:
name: publish-python-sdk
needs: build-python-sdk
needs:
- build-python-sdk
- publish-python-runtime
- resolve-python-release
runs-on: ubuntu-latest
environment: pypi
permissions:
@@ -225,6 +191,11 @@ jobs:
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download Python SDK package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -235,3 +206,14 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/python-sdk
skip-existing: true
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Verify Python SDK is available on PyPI
env:
SDK_VERSION: ${{ needs.resolve-python-release.outputs.sdk_version }}
run: uv run --no-project --with packaging==26.2 python .github/scripts/verify_pypi_release.py openai-codex "$SDK_VERSION"

View File

@@ -26,6 +26,14 @@ jobs:
- name: Verify Bazel clippy flags match Cargo workspace lints
run: python3 .github/scripts/verify_bazel_clippy_lints.py
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Test PyPI release verification
run: uv run --no-project --with packaging==26.2 python -m unittest discover -s .github/scripts -p 'test_verify_pypi_release.py'
- name: Test Codex package builder
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
@@ -53,10 +61,6 @@ jobs:
- name: Check root README ToC
run: python3 scripts/readme_toc.py README.md
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Check formatting (run `just fmt` to fix)
run: just fmt-check