diff --git a/.github/scripts/publish_r2_release.py b/.github/scripts/publish_r2_release.py new file mode 100755 index 0000000000..62a01dfb15 --- /dev/null +++ b/.github/scripts/publish_r2_release.py @@ -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//``, 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()) diff --git a/.github/scripts/publish_r2_release.py.lock b/.github/scripts/publish_r2_release.py.lock new file mode 100644 index 0000000000..64ecbfeda1 --- /dev/null +++ b/.github/scripts/publish_r2_release.py.lock @@ -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" }, +] diff --git a/.github/workflows/r2-release.yml b/.github/workflows/r2-release.yml new file mode 100644 index 0000000000..69e256fd95 --- /dev/null +++ b/.github/workflows/r2-release.yml @@ -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}"