mirror of
https://github.com/openai/codex.git
synced 2026-09-03 14:59:03 +00:00
Publish Codex installer assets to R2
Co-authored-by: Zsolt Dollenstein <zsol.zsol@gmail.com>
This commit is contained in:
459
.github/scripts/publish_r2_release.py
vendored
Executable file
459
.github/scripts/publish_r2_release.py
vendored
Executable 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())
|
||||
113
.github/scripts/publish_r2_release.py.lock
vendored
Normal file
113
.github/scripts/publish_r2_release.py.lock
vendored
Normal 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
45
.github/workflows/r2-release.yml
vendored
Normal 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}"
|
||||
Reference in New Issue
Block a user