mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## What changed Add a downstream workflow that builds the Python SDK and runtime from the stable CLI release commit, using the CLI version for both packages and the SDK's exact runtime dependency. Publish and verify the runtime on PyPI before publishing the SDK. Require a successful CLI `release` job, an unchanged release tag, and complete runtime assets. Skip CLI prereleases and allow publication despite unrelated publisher failures. Support retries by CLI workflow run ID and accept existing PyPI uploads while verifying the complete release. Document release setup, retry procedures, and independent SDK releases. ## Testing Add resolver unit tests covering tag resolution, prerelease skipping, partial reruns, pagination, invalid runs, moved tags, missing assets, and equivalent automatic and manual release resolution. GitOrigin-RevId: 4ec6b2e77c94c851507dbe7200ea420994fce36e
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Bind downstream Python publication to a published stable CLI release."""
|
|
|
|
import argparse
|
|
import itertools
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
def github_api(path: str):
|
|
return json.loads(subprocess.check_output(["gh", "api", path], text=True))
|
|
|
|
|
|
def github_items(path: str, key: str | None = None):
|
|
separator = "&" if "?" in path else "?"
|
|
for page in itertools.count(1):
|
|
response = github_api(f"{path}{separator}per_page=100&page={page}")
|
|
items = response[key] if key else response
|
|
yield from items
|
|
if len(items) < 100:
|
|
return
|
|
|
|
|
|
def resolve_release(
|
|
repository: str, run_id: str, *, run: dict | None = None
|
|
) -> dict[str, str] | None:
|
|
if repository != "openai/codex" or not re.fullmatch(r"[1-9][0-9]*", run_id):
|
|
raise ValueError("Expected an openai/codex Rust release run ID")
|
|
prefix = f"repos/{repository}"
|
|
if run is None:
|
|
run = github_api(f"{prefix}/actions/runs/{run_id}")
|
|
if (
|
|
str(run["id"]) != run_id
|
|
or run["path"] != ".github/workflows/rust-release.yml"
|
|
or run["event"] != "push"
|
|
or run["status"] != "completed"
|
|
or run["head_repository"]["full_name"] != repository
|
|
):
|
|
raise ValueError("Python publication requires a completed Rust release run")
|
|
tag = run["head_branch"]
|
|
if not isinstance(tag, str) or not tag.startswith("rust-v"):
|
|
raise ValueError("The Rust release run must identify a release tag")
|
|
match = re.fullmatch(r"rust-v([0-9]+\.[0-9]+\.[0-9]+)", tag)
|
|
if match is None:
|
|
return None # CLI prereleases do not publish the Python SDK.
|
|
revision = run["head_sha"]
|
|
if not re.fullmatch(r"[0-9a-f]{40}", revision):
|
|
raise ValueError("The Rust release run must identify an exact commit")
|
|
|
|
# A partial rerun of an unrelated publisher may not contain the release job.
|
|
# Inspect all attempts and use the last release job that actually ran.
|
|
release_job = max(
|
|
(
|
|
job
|
|
for job in github_items(
|
|
f"{prefix}/actions/runs/{run_id}/jobs?filter=all", "jobs"
|
|
)
|
|
if job["name"] == "release" and job["conclusion"] != "skipped"
|
|
),
|
|
key=lambda job: (job["run_attempt"], job["id"]),
|
|
default=None,
|
|
)
|
|
if release_job is None or release_job["conclusion"] != "success":
|
|
raise ValueError("Python publication requires a successful release job")
|
|
|
|
target = github_api(f"{prefix}/git/ref/tags/{tag}")["object"]
|
|
for _ in range(8):
|
|
if target["type"] != "tag":
|
|
break
|
|
target = github_api(f"{prefix}/git/tags/{target['sha']}")["object"]
|
|
if target.get("type") != "commit" or target.get("sha") != revision:
|
|
raise ValueError("The release tag no longer matches the successful CLI run")
|
|
release = github_api(f"{prefix}/releases/tags/{tag}")
|
|
if release["draft"] or release["prerelease"] or release["tag_name"] != tag:
|
|
raise ValueError("The CLI release must be published and stable")
|
|
|
|
# The runtime builder downloads these six wheels and builds the two musl
|
|
# wheels from the release's package archives.
|
|
required_assets = {
|
|
f"openai_codex_cli_bin-{match[1]}-py3-none-{platform}.whl"
|
|
for platform in (
|
|
"macosx_10_9_x86_64",
|
|
"macosx_11_0_arm64",
|
|
"manylinux_2_17_aarch64",
|
|
"manylinux_2_17_x86_64",
|
|
"win_amd64",
|
|
"win_arm64",
|
|
)
|
|
} | {
|
|
f"codex-package-{arch}-unknown-linux-musl.tar.gz"
|
|
for arch in ("aarch64", "x86_64")
|
|
}
|
|
available_assets = {
|
|
asset["name"]
|
|
for asset in github_items(f"{prefix}/releases/{release['id']}/assets")
|
|
if asset["state"] == "uploaded" and asset["size"] > 0
|
|
}
|
|
if missing := required_assets - available_assets:
|
|
raise ValueError(f"The CLI release is missing Python inputs: {sorted(missing)}")
|
|
return {"source_sha": revision, "release_tag": tag, "version": match[1]}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("run_id")
|
|
parser.add_argument("--repository", required=True)
|
|
parser.add_argument("--event-path", type=Path)
|
|
parser.add_argument("--github-output", type=Path, required=True)
|
|
args = parser.parse_args(argv)
|
|
event = json.loads(args.event_path.read_text()) if args.event_path else {}
|
|
release = resolve_release(
|
|
args.repository, args.run_id, run=event.get("workflow_run")
|
|
)
|
|
with args.github_output.open("a") as output:
|
|
print(f"publish={str(release is not None).lower()}", file=output)
|
|
for name, value in (release or {}).items():
|
|
print(f"{name}={value}", file=output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|