Publish Codex installer assets to R2

Co-authored-by: Zsolt Dollenstein <zsol.zsol@gmail.com>
This commit is contained in:
Zsolt Dollenstein
2026-07-09 15:05:03 +01:00
parent f1affbac5e
commit 6545fccc0f
3 changed files with 617 additions and 0 deletions

459
.github/scripts/publish_r2_release.py vendored Executable file
View File

@@ -0,0 +1,459 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "boto3>=1.43.39",
# "ty==0.0.57",
# ]
# ///
"""Validate and publish Codex installer release assets.
The script finds the GitHub Release for a completed ``rust-release`` workflow,
downloads its seven installer assets, verifies the package checksums, then
publishes them to R2. It does not contact S3 until the downloads are verified.
The S3 client uses these standard AWS environment variables:
* ``AWS_ENDPOINT_URL`` (the R2 S3 endpoint)
* ``AWS_ACCESS_KEY_ID``
* ``AWS_SECRET_ACCESS_KEY``
* ``AWS_SESSION_TOKEN`` when the credential service issues one
* ``AWS_REGION`` or ``AWS_DEFAULT_REGION`` when required by the S3 client
This script constructs only ``codex/`` keys and calls HeadObject, GetObject,
and PutObject. It uploads the installer package archives and checksum manifest
to ``codex/releases/<version>/``, verifies each read-back, then publishes an
immutable version manifest. Exact existing objects are accepted so interrupted
runs can resume, but conflicting objects fail verification. The script does not
manage release channels.
"""
import argparse
import hashlib
import json
import os
import re
import shutil
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from contextlib import closing
from dataclasses import dataclass
from pathlib import Path
from typing import NoReturn, cast
import boto3
from botocore.exceptions import BotoCoreError, ClientError
BUCKET = "releases"
PREFIX = "codex"
MANIFEST_NAME = "manifest.json"
SCHEMA_VERSION = 1
REPOSITORY = "openai/codex"
API_ROOT = "https://api.github.com"
API_VERSION = "2022-11-28"
VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+)?)?$")
CHECKSUM_RE = re.compile(r"^([0-9a-f]{64}) ([A-Za-z0-9_.-]+)$")
INSTALLER_ASSETS = (
"codex-package-aarch64-apple-darwin.tar.gz",
"codex-package-x86_64-apple-darwin.tar.gz",
"codex-package-aarch64-unknown-linux-musl.tar.gz",
"codex-package-x86_64-unknown-linux-musl.tar.gz",
"codex-package-aarch64-pc-windows-msvc.tar.gz",
"codex-package-x86_64-pc-windows-msvc.tar.gz",
"codex-package_SHA256SUMS",
)
PACKAGE_ASSETS = INSTALLER_ASSETS[:-1]
CHECKSUM_ASSET = INSTALLER_ASSETS[-1]
class PublishError(RuntimeError):
pass
@dataclass(frozen=True)
class Artifact:
source: Path
path: str
key: str
size: int
sha256: str
@dataclass(frozen=True)
class ValidatedRelease:
tag: str
version: str
class BotoS3Client:
"""Small S3 adapter that leaves credential resolution to boto3."""
def __init__(self, bucket: str = BUCKET) -> None:
endpoint = os.environ.get("AWS_ENDPOINT_URL")
if not endpoint:
raise PublishError("AWS_ENDPOINT_URL is required for the R2 S3 endpoint")
self.bucket = bucket
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
self.client = boto3.client(
"s3",
endpoint_url=endpoint,
region_name=region,
)
def exists(self, key: str) -> bool:
try:
self.client.head_object(Bucket=self.bucket, Key=key)
return True
except ClientError as error:
code = str(error.response.get("Error", {}).get("Code", ""))
if code in {"404", "NoSuchKey", "NotFound"}:
return False
self._raise("check", key, error)
except BotoCoreError as error:
self._raise("check", key, error)
def put_file(self, key: str, path: Path) -> None:
try:
with path.open("rb") as body:
self.client.put_object(
Bucket=self.bucket,
Key=key,
Body=body,
IfNoneMatch="*",
)
except (BotoCoreError, ClientError) as error:
self._raise("upload", key, error)
def put_bytes(
self,
key: str,
contents: bytes,
content_type: str,
) -> None:
try:
self.client.put_object(
Bucket=self.bucket,
Key=key,
Body=contents,
ContentType=content_type,
IfNoneMatch="*",
)
except (BotoCoreError, ClientError) as error:
self._raise("upload", key, error)
def get_file(self, key: str, path: Path) -> None:
try:
response = self.client.get_object(Bucket=self.bucket, Key=key)
with closing(response["Body"]), path.open("wb") as destination:
shutil.copyfileobj(response["Body"], destination)
except (BotoCoreError, ClientError) as error:
self._raise("read back", key, error)
def get_bytes(self, key: str) -> bytes:
try:
response = self.client.get_object(Bucket=self.bucket, Key=key)
with closing(response["Body"]):
return response["Body"].read()
except (BotoCoreError, ClientError) as error:
self._raise("read back", key, error)
def _raise(self, action: str, key: str, error: Exception) -> NoReturn:
raise PublishError(
f"could not {action} s3://{self.bucket}/{key}: {error}"
) from error
class GitHubApi:
def __init__(self, token: str) -> None:
self.token = token
def get_json(self, path: str) -> dict[str, object]:
if not self.token:
raise PublishError("GITHUB_TOKEN is required for GitHub API metadata")
if not path.startswith("/"):
raise PublishError(f"invalid GitHub API path: {path}")
request = urllib.request.Request(
f"{API_ROOT}{path}",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {self.token}",
"User-Agent": "codex-r2-release-publisher",
"X-GitHub-Api-Version": API_VERSION,
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
value = json.load(response)
except (OSError, urllib.error.URLError, json.JSONDecodeError) as error:
raise PublishError(
f"GitHub API request failed for {path}: {error}"
) from error
return require_object(value, f"GitHub response for {path}")
def download_asset(
self,
url: str,
destination: Path,
) -> None:
request = urllib.request.Request(
url,
headers={
"Accept": "application/octet-stream",
"User-Agent": "codex-r2-release-publisher",
},
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
with destination.open("xb") as output:
while chunk := response.read(1024 * 1024):
output.write(chunk)
except (OSError, urllib.error.URLError) as error:
destination.unlink(missing_ok=True)
raise PublishError(
f"GitHub asset download failed for {destination.name}: {error}"
) from error
def require_object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict):
raise PublishError(f"{label} must be an object")
return cast(dict[str, object], value)
def require_list(value: object, label: str) -> list[object]:
if not isinstance(value, list):
raise PublishError(f"{label} must be a list")
return cast(list[object], value)
def require_str(value: object, label: str) -> str:
if not isinstance(value, str) or not value:
raise PublishError(f"{label} must be a non-empty string")
return value
def release_asset_url(tag: str, name: str) -> str:
return f"https://github.com/{REPOSITORY}/releases/download/{tag}/{name}"
def require_installer_assets(release: dict[str, object]) -> None:
assets = require_list(release.get("assets"), "release assets")
names = [
require_str(require_object(asset, "release asset").get("name"), "asset name")
for asset in assets
]
for name in INSTALLER_ASSETS:
count = names.count(name)
if count != 1:
raise PublishError(
f"expected one release asset named {name}, found {count}"
)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
while chunk := source.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def sha256_bytes(contents: bytes) -> str:
return hashlib.sha256(contents).hexdigest()
def parse_checksums(path: Path) -> dict[str, str]:
try:
lines = path.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as error:
raise PublishError(f"could not read checksum manifest: {error}") from error
checksums: dict[str, str] = {}
for line_number, line in enumerate(lines, start=1):
match = CHECKSUM_RE.fullmatch(line)
if not match:
raise PublishError(f"invalid checksum manifest line {line_number}")
digest, name = match.groups()
if name in checksums:
raise PublishError(f"duplicate checksum manifest entry: {name}")
checksums[name] = digest
return checksums
def verify_package_checksums(dist: Path) -> None:
checksums = parse_checksums(dist / CHECKSUM_ASSET)
for name in PACKAGE_ASSETS:
if checksums.get(name) != sha256_file(dist / name):
raise PublishError(f"checksum mismatch for release asset {name}")
def validate_release(
event: dict[str, object],
api: GitHubApi,
) -> ValidatedRelease:
run = require_object(event.get("workflow_run"), "workflow_run")
tag = require_str(run.get("head_branch"), "workflow run tag")
version = tag.removeprefix("rust-v")
if tag == version or not VERSION_RE.fullmatch(version):
raise PublishError(f"invalid rust release tag: {tag}")
quoted_tag = urllib.parse.quote(tag, safe="")
release = api.get_json(f"/repos/{REPOSITORY}/releases/tags/{quoted_tag}")
require_installer_assets(release)
return ValidatedRelease(tag=tag, version=version)
def load_event(path: Path) -> dict[str, object]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as error:
raise PublishError(f"could not read workflow_run event: {error}") from error
return require_object(value, "workflow_run event")
def download_release_assets(
release: ValidatedRelease, dist: Path, api: GitHubApi
) -> None:
try:
dist.mkdir(parents=True, exist_ok=False)
except OSError as error:
raise PublishError(
f"could not create isolated dist directory: {error}"
) from error
for name in INSTALLER_ASSETS:
api.download_asset(release_asset_url(release.tag, name), dist / name)
verify_package_checksums(dist)
def validate_version(version: str) -> None:
if not VERSION_RE.fullmatch(version):
raise PublishError(f"invalid Codex release version: {version}")
def release_root(version: str) -> str:
return f"{PREFIX}/releases/{version}"
def artifacts_for(dist: Path, version: str) -> list[Artifact]:
if not dist.is_dir():
raise PublishError(f"dist directory does not exist: {dist}")
root = release_root(version)
artifacts = []
for name in INSTALLER_ASSETS:
matches = sorted(path for path in dist.rglob(name) if path.is_file())
if len(matches) != 1:
raise PublishError(
f"expected exactly one installer asset named {name}, found {len(matches)}"
)
path = matches[0]
if path.is_symlink():
raise PublishError(f"refusing symlink in dist: {path}")
artifacts.append(
Artifact(
source=path,
path=name,
key=f"{root}/{name}",
size=path.stat().st_size,
sha256=sha256_file(path),
)
)
return artifacts
def manifest_bytes(version: str, artifacts: list[Artifact]) -> bytes:
manifest = {
"artifacts": [
{
"key": artifact.key,
"path": artifact.path,
"sha256": artifact.sha256,
"size": artifact.size,
}
for artifact in artifacts
],
"product": "codex",
"releasePrefix": f"{release_root(version)}/",
"schemaVersion": SCHEMA_VERSION,
"version": version,
}
return (json.dumps(manifest, indent=2, sort_keys=True) + "\n").encode()
def verify_file(client: BotoS3Client, key: str, expected: Artifact) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
downloaded = Path(temp_dir) / "readback"
client.get_file(key, downloaded)
actual_size = downloaded.stat().st_size
actual_sha256 = sha256_file(downloaded)
if actual_size != expected.size or actual_sha256 != expected.sha256:
raise PublishError(
f"read-back mismatch for {key}: expected size={expected.size} "
f"sha256={expected.sha256}, got size={actual_size} sha256={actual_sha256}"
)
def verify_bytes(client: BotoS3Client, key: str, expected: bytes) -> None:
actual = client.get_bytes(key)
if actual != expected:
raise PublishError(
f"read-back mismatch for {key}: expected size={len(expected)} "
f"sha256={sha256_bytes(expected)}, got size={len(actual)} "
f"sha256={sha256_bytes(actual)}"
)
def publish(dist: Path, version: str, client: BotoS3Client) -> dict[str, object]:
validate_version(version)
artifacts = artifacts_for(dist, version)
for artifact in artifacts:
if not client.exists(artifact.key):
client.put_file(artifact.key, artifact.source)
verify_file(client, artifact.key, artifact)
manifest_key = f"{release_root(version)}/{MANIFEST_NAME}"
manifest = manifest_bytes(version, artifacts)
if not client.exists(manifest_key):
client.put_bytes(
manifest_key,
manifest,
content_type="application/json",
)
verify_bytes(client, manifest_key, manifest)
return {
"artifacts": len(artifacts),
"manifestKey": manifest_key,
"manifestSha256": sha256_bytes(manifest),
"version": version,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--event", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
api = GitHubApi(os.environ.get("GITHUB_TOKEN", ""))
release = validate_release(load_event(args.event), api)
with tempfile.TemporaryDirectory() as temp_dir:
dist = Path(temp_dir) / "dist"
download_release_assets(release, dist, api)
receipt = publish(dist, release.version, BotoS3Client())
receipt["tag"] = release.tag
except PublishError as error:
print(f"publish failed: {error}", file=sys.stderr)
return 1
print(json.dumps(receipt, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,113 @@
version = 1
revision = 3
requires-python = ">=3.10"
[manifest]
requirements = [
{ name = "boto3", specifier = ">=1.43.39" },
{ name = "ty", specifier = "==0.0.57" },
]
[[package]]
name = "boto3"
version = "1.43.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/bd/4d27e9002536dcf85cf97126d4fe93cc01f3a73000f14408ec51554231ca/boto3-1.43.44.tar.gz", hash = "sha256:035d73afe3e29bf271a5b27b30476ff8eefa5ae36f6702eada8e412d5c1420aa", size = 112697, upload-time = "2026-07-09T00:38:47.382Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/f2/48081d32e813b14ec03495737984208f193d9a102820b25844c61c94d827/boto3-1.43.44-py3-none-any.whl", hash = "sha256:621113f7850caec7c8405a07baa26075f700f78844a7f2fdf4d1f879bd3cc3c1", size = 140029, upload-time = "2026-07-09T00:38:45.63Z" },
]
[[package]]
name = "botocore"
version = "1.43.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/9e/ad63ae4996794be10afef4e1b2f98901947da65c6c349be9c063ee73edbf/botocore-1.43.44.tar.gz", hash = "sha256:cc2a95a53efdcf0ce34a51bca28058e72fcc3b10dd625eb1ad900c5ca3eb8bef", size = 15685774, upload-time = "2026-07-09T00:38:36.71Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/05/5c5de6ed23b5e1b01e36b9e52f056dc2b09b259daf3371e4b615ad9a1d4a/botocore-1.43.44-py3-none-any.whl", hash = "sha256:6afb846fd93815133a53baf1578f1d6016ddbeda247623360379758ca2e3f338", size = 15374229, upload-time = "2026-07-09T00:38:33.716Z" },
]
[[package]]
name = "jmespath"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "s3transfer"
version = "0.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "ty"
version = "0.0.57"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/16/d01c968d405acae51c07872e80f30f3a586235bdf52c9847ca0917a230a3/ty-0.0.57.tar.gz", hash = "sha256:bc058f564868690283a0420d09c269ec8be21e8e43b4b49ee975a17623092e44", size = 6100787, upload-time = "2026-07-08T11:33:59.904Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/9c/a7948b05f2a3f43d511f88ef5c4f56d7edb8acc8caa5f56d7c5831f52c84/ty-0.0.57-py3-none-linux_armv6l.whl", hash = "sha256:cb6d3371dd8c78950b75bee31a36b94564a54a1f0eecafbbf05715ac1a5287d6", size = 11760058, upload-time = "2026-07-08T11:33:18.671Z" },
{ url = "https://files.pythonhosted.org/packages/6e/92/3776380decba3965bcaa1ea2b56f5b133aa3ef549bfbe4b79eb122dcc15d/ty-0.0.57-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e58b90491a48ec757bd50f512f3eb92c1c64b7d46a4db83677e9b60222e1d2bb", size = 11492105, upload-time = "2026-07-08T11:33:21.267Z" },
{ url = "https://files.pythonhosted.org/packages/13/be/912f8422d06fd1e29805a7c781e3ce4c821917e0e3d00f0cf176c0529469/ty-0.0.57-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dbb8207f75122c658ca21bf405cea8202e490240440461ae3e0c5a6f67ae668f", size = 11078675, upload-time = "2026-07-08T11:33:23.804Z" },
{ url = "https://files.pythonhosted.org/packages/06/86/6ed526df554b491ce3d07bbec04f7a10304243bc994ab989352c85134b1e/ty-0.0.57-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:651243f391809de80b01be1729c5d0cecc7b952d7d22cfbf56f0b4f069703195", size = 11614379, upload-time = "2026-07-08T11:33:26.041Z" },
{ url = "https://files.pythonhosted.org/packages/0d/95/78af20f309abce31fa616e0142f85756e665a9965669b823181ff695ffec/ty-0.0.57-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28c5392bbd7c4d6a4c1b1646a709231db675dc094f49bf71b7de83757125e93b", size = 11563854, upload-time = "2026-07-08T11:33:28.144Z" },
{ url = "https://files.pythonhosted.org/packages/88/dc/bf9231d5563b9e61a1eec9782184a4055afaa87259644cd9ed1376a7dc5c/ty-0.0.57-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d981535094ab492aaef6f71d9348bcf90e42e6de4cb50de2dd7fbabd8378360", size = 12229897, upload-time = "2026-07-08T11:33:30.262Z" },
{ url = "https://files.pythonhosted.org/packages/c0/dd/012008190a997097ebe500b9704baaad9135bfa033b9530a9b8f69f1d11f/ty-0.0.57-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:719b41fd6df61352866cad952d7bfb4873c893a70e806d37d0f1d30ad4f26cd5", size = 12816002, upload-time = "2026-07-08T11:33:32.747Z" },
{ url = "https://files.pythonhosted.org/packages/f4/2f/0e42c361e38e04747ed2b3d3299512edd4ea6060d8e3124e3c6f2d2465a1/ty-0.0.57-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1641a609b025e13f44115c7071b39b02675044a78fbfedef239a5f7da50b393", size = 12323420, upload-time = "2026-07-08T11:33:35.426Z" },
{ url = "https://files.pythonhosted.org/packages/c6/01/18f1e03108d1ae8e515c92fb304994a66eaafd47037f928fd42fd3a1e29d/ty-0.0.57-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33f96a9dd6919fe8b1d3512cd99f16be4a51388f265de135fea2c2fbf0b4317", size = 12119481, upload-time = "2026-07-08T11:33:37.808Z" },
{ url = "https://files.pythonhosted.org/packages/6a/48/570b73fb3e32554ff03aa9f6650b33a504cdfa027a8b50a5ab087e56b20d/ty-0.0.57-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f0a1014a922b2b7f79a46e5cdbaa6feb7403a444631292b8666382a98a04de20", size = 12427299, upload-time = "2026-07-08T11:33:39.964Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/76b27f6a92e12525d09cd65d3c3b5aea419cf4782b13320db95ac3d310f0/ty-0.0.57-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d73aaed84023bf682819f871377b255fae9098898c50475426ac9bdfcd986872", size = 11562292, upload-time = "2026-07-08T11:33:42.392Z" },
{ url = "https://files.pythonhosted.org/packages/84/8b/d0c398147b00f336595e1a00a5895b3924251205c11b8d73cb0668281f1c/ty-0.0.57-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3e2104704063c00ab8a757af3e95378b32624a3e8d8149aad5251877bf82959", size = 11565833, upload-time = "2026-07-08T11:33:44.778Z" },
{ url = "https://files.pythonhosted.org/packages/3e/74/dab9745e977ef38751ec710f74e40d21fddbd04a80338dd5597c98899eed/ty-0.0.57-py3-none-musllinux_1_2_i686.whl", hash = "sha256:07ad760763646d8f1567ea5d899e6d217212acd8539b7afed5a370d93693553b", size = 11864967, upload-time = "2026-07-08T11:33:47.36Z" },
{ url = "https://files.pythonhosted.org/packages/a2/57/350812143b49dac7aa655f40a1f45d4821ca9b56b75d9b68d5e603269044/ty-0.0.57-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4a6e1f0fee7df3e65fb0b9cbe9f99a4be39198558ed9aacc31a98d210ad7bcc", size = 12239054, upload-time = "2026-07-08T11:33:49.586Z" },
{ url = "https://files.pythonhosted.org/packages/a4/d0/b3e3d9c6cce3debda6247aa435e81d18ec62fabfec13f35f6c6957066f47/ty-0.0.57-py3-none-win32.whl", hash = "sha256:f8d488c0535a8f0386dbe2c9bcb31d467ae0c68d0c9945113018937828ebaabb", size = 11225353, upload-time = "2026-07-08T11:33:52.569Z" },
{ url = "https://files.pythonhosted.org/packages/77/db/6ce240ee31413f9dc7467e31377553e92e2664252ee7d33aa9290a7a94bb/ty-0.0.57-py3-none-win_amd64.whl", hash = "sha256:de9529a7dcc3e529b08c14634dc4e7066ebea5cd55006afc02bde8033efe7c91", size = 12272419, upload-time = "2026-07-08T11:33:54.889Z" },
{ url = "https://files.pythonhosted.org/packages/44/de/48662fa2c42289f309eeb8b5539cbe840e10002b65ac0700cb7889e92532/ty-0.0.57-py3-none-win_arm64.whl", hash = "sha256:7f3352777ce40c4906145f3c3e10b71716d8d35c0c9e3a0070d84f61dda0755f", size = 11686309, upload-time = "2026-07-08T11:33:57.246Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]

45
.github/workflows/r2-release.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: publish-r2-release
on:
workflow_run:
workflows: [rust-release]
types: [completed]
permissions: {}
concurrency:
group: publish-r2-release-${{ github.event.workflow_run.id }}
cancel-in-progress: false
jobs:
publish:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 60
environment: codex-r2-publisher
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.28"
- name: Validate and publish installer assets to R2
env:
GITHUB_TOKEN: ${{ github.token }}
AWS_ACCESS_KEY_ID: ${{ secrets.CODEX_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CODEX_R2_SECRET_ACCESS_KEY }}
AWS_ENDPOINT_URL: ${{ vars.CODEX_R2_ENDPOINT_URL }}
AWS_REGION: ${{ vars.CODEX_R2_REGION }}
run: |
set -euo pipefail
uv run --locked --script .github/scripts/publish_r2_release.py \
--event "${GITHUB_EVENT_PATH}"