Files
codex/.github/scripts/contract_releases.py
felixxia-oai b5115cdd66 Resolve stable releases dynamically for exec-server compatibility tests (#54623)
## What changed

Replace fixed release pins for app-server/exec-server compatibility tests with the latest stable version and the stable release published closest to 60 days ago. Exclude drafts and prereleases, and prefer the higher version when publication dates are equally close.

Resolve release versions and SHA-256 asset digests before Linux Bazel CI runs, passing them through `CODEX_COMPAT_RELEASES` to the release archive repositories. Reuse the argument comment lint action so it also prepares compatibility releases. Add bounded retries for transient GitHub release reads.

## Testing

Add unit coverage for stable release filtering, version ordering, nearest publication dates, tie-breaking, and deduplication when both selections resolve to the same release.

GitOrigin-RevId: 5a8cf8bdabd77ab3fa89bbb711367b5d9ce8044b
2026-10-16 14:49:23 +00:00

127 lines
4.4 KiB
Python

"""Discover and download published Codex packages for contract runners."""
import json
import re
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
ALPHA_TAG = re.compile(r"rust-v[0-9]+\.[0-9]+\.[0-9]+-alpha(?:\.[0-9]+){0,2}")
RELEASE_TAG = re.compile(r"rust-v[0-9]+\.[0-9]+\.[0-9]+(?:-alpha(?:\.[0-9]+){0,2})?")
STABLE_TAG = re.compile(r"rust-v[0-9]+\.[0-9]+\.[0-9]+")
PACKAGE = "codex-package-x86_64-unknown-linux-musl.tar.gz"
def github_read(args: list[str]) -> str:
"""Retry release reads unless the error is known permanent; never retry verification."""
for attempt, delay in enumerate((2, 5, 0), start=1):
try:
return subprocess.check_output(
args, text=True, stderr=subprocess.PIPE, timeout=120
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
reason = "timeout"
exit_code = None
http_status = "unknown"
retryable = True
if isinstance(error, subprocess.CalledProcessError):
message = error.stderr or ""
status = re.search(r"\bHTTP (\d{3})\b", message)
reason = "command_failed"
exit_code = error.returncode
http_status = status[1] if status else "unknown"
# gh uses 1 for general failures, 2 for cancellation and 4 for auth.
# Unknown transport/protocol wording should still get bounded retries.
retryable = (
error.returncode == 1
and (status is None or status[1] in {"502", "503", "504"})
and not any(
marker in message.lower()
for marker in (
"release not found",
"no assets match",
"no assets to download",
"unknown flag",
"unknown command",
)
)
)
# Only emit classified fields; stderr can contain unknown credential formats.
will_retry = retryable and bool(delay)
print(
f"GitHub release read failed: attempt={attempt} reason={reason} "
f"exit_code={exit_code} http_status={http_status} "
f"will_retry={str(will_retry).lower()}",
file=sys.stderr,
)
if not will_retry:
raise
print(f"Retrying GitHub release read in {delay}s.", file=sys.stderr)
time.sleep(delay)
def list_releases(*, exclude_prereleases: bool = False) -> list[dict]:
# GraphQL-backed listing avoids fetching assets and release notes.
args = [
"gh",
"release",
"list",
"--repo",
"openai/codex",
"--limit",
"100",
"--json",
"tagName,publishedAt,isDraft,isPrerelease",
]
if exclude_prereleases:
args.append("--exclude-pre-releases")
return json.loads(github_read(args))
def download_release(tag: str, directory: Path) -> Path:
github_read(
[
"gh",
"release",
"download",
tag,
"--repo",
"openai/codex",
"--pattern",
PACKAGE,
"--dir",
str(directory),
# A failed download can leave a partial file in this temp directory.
"--clobber",
]
)
return directory / PACKAGE
def select_stables(releases: list[dict], target_date: datetime) -> list[str]:
stable = [
release
for release in releases
if not release["isDraft"]
and not release["isPrerelease"]
and STABLE_TAG.fullmatch(release["tagName"])
]
stable.sort(
key=lambda release: tuple(map(int, release["tagName"][6:].split("."))),
reverse=True,
)
if not stable:
raise ValueError("No stable releases available")
# The descending version order breaks equal-distance ties deterministically.
baseline = min(
stable,
key=lambda release: abs(
datetime.fromisoformat(release["publishedAt"].replace("Z", "+00:00"))
- target_date
),
)
# A long release pause can make latest and historical resolve to one build.
return list(dict.fromkeys([stable[0]["tagName"], baseline["tagName"]]))