fix(install): reuse GitHub release metadata (#31056)

## Why

The standalone installers currently perform separate unauthenticated
GitHub REST API lookups while resolving the latest version, locating the
platform package, locating its checksum manifest, and retrieving asset
digests. A single install can therefore make up to four release-metadata
requests.

When GitHub's shared unauthenticated rate limit is exhausted, valid
releases fail to install. The shell installer also suppresses the
metadata request failure while probing assets, so a `403` is misreported
as though the release assets do not exist. This makes the failure both
more likely and harder to diagnose.

Fixes #28538.

## What changed

- Resolve the selected version and fetch its release metadata together.
- Reuse that one metadata response for package, checksum, and
legacy-package selection in both `install.sh` and `install.ps1`.
- Report metadata fetch failures as possible GitHub availability or
rate-limit failures instead of missing assets.
- Add a mocked-`curl` regression suite covering exact releases,
`latest`, and a simulated metadata `403`, and run it in `repo-checks`.

For `latest`, the metadata returned by `/releases/latest` now supplies
both the resolved version and the asset list. For an explicitly selected
version, the installer makes one request to that release's tag endpoint.

## Verification

- `python3 -m unittest discover -s scripts/install -p 'test_*.py' -v`
- `sh -n scripts/install/install.sh`
- Parsed `scripts/install/install.ps1` with the PowerShell language
parser.

## Scope

This change reduces GitHub API usage and preserves the underlying error,
but it does not move release artifacts away from GitHub's CDN.
This commit is contained in:
Michael Bolin
2026-07-03 15:11:19 -07:00
committed by GitHub
parent 1f17e7512f
commit 319d03056e
4 changed files with 232 additions and 64 deletions

View File

@@ -28,6 +28,9 @@ jobs:
- name: Test Codex package builder
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
- name: Test standalone installer
run: python3 -m unittest discover -s scripts/install -p 'test_*.py'
- name: Setup pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:

View File

@@ -79,11 +79,10 @@ function Assert-ValidReleaseVersion {
function Find-ReleaseAssetMetadata {
param(
[string]$AssetName,
[string]$ResolvedVersion
[object]$ReleaseMetadata
)
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/tags/rust-v$ResolvedVersion"
$asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
$asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
if ($null -eq $asset) {
return $null
}
@@ -99,20 +98,6 @@ function Find-ReleaseAssetMetadata {
}
}
function Get-ReleaseAssetMetadata {
param(
[string]$AssetName,
[string]$ResolvedVersion
)
$metadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ResolvedVersion $ResolvedVersion
if ($null -eq $metadata) {
throw "Could not find release asset $AssetName for Codex $ResolvedVersion."
}
return $metadata
}
function Test-ArchiveDigest {
param(
[string]$ArchivePath,
@@ -216,22 +201,38 @@ function Remove-StaleInstallArtifacts {
}
}
function Resolve-Version {
function Resolve-Release {
$normalizedVersion = Normalize-Version -RawVersion $Release
Assert-ValidReleaseVersion -Version $normalizedVersion
if ($normalizedVersion -ne "latest") {
return $normalizedVersion
if ($normalizedVersion -eq "latest") {
$requestedRelease = "latest"
$metadataUri = "https://api.github.com/repos/openai/codex/releases/latest"
} else {
$resolvedVersion = $normalizedVersion
$requestedRelease = $resolvedVersion
$metadataUri = "https://api.github.com/repos/openai/codex/releases/tags/rust-v$resolvedVersion"
}
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/latest"
if (-not $release.tag_name) {
Write-Error "Failed to resolve the latest Codex release version."
exit 1
try {
$releaseMetadata = Invoke-RestMethod -Uri $metadataUri
} catch {
throw "Could not fetch GitHub release metadata for Codex $requestedRelease. GitHub API may be unavailable or rate limited. $($_.Exception.Message)"
}
$resolvedVersion = Normalize-Version -RawVersion $release.tag_name
Assert-ValidReleaseVersion -Version $resolvedVersion
return $resolvedVersion
if ($normalizedVersion -eq "latest") {
if (-not $releaseMetadata.tag_name) {
throw "Failed to resolve the latest Codex release version."
}
$resolvedVersion = Normalize-Version -RawVersion $releaseMetadata.tag_name
Assert-ValidReleaseVersion -Version $resolvedVersion
}
return [PSCustomObject]@{
Version = $resolvedVersion
Metadata = $releaseMetadata
}
}
function Get-VersionFromBinary {
@@ -746,7 +747,9 @@ if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALL_DIR)) {
}
$currentVersion = Get-CurrentInstalledVersion -StandaloneCurrentDir $currentDir
$resolvedVersion = Resolve-Version
$resolvedRelease = Resolve-Release
$resolvedVersion = $resolvedRelease.Version
$releaseMetadata = $resolvedRelease.Metadata
$releaseName = "$resolvedVersion-$target"
$releaseDir = Join-Path $releasesDir $releaseName
@@ -765,12 +768,12 @@ $oldStandaloneBackup = $null
$packageAsset = "codex-package-$target.tar.gz"
$checksumAsset = "codex-package_SHA256SUMS"
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ResolvedVersion $resolvedVersion
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata
$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata
$installLayout = "Package"
if ($null -eq $packageMetadata -or $null -eq $checksumMetadata) {
$packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz"
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata
if ($null -ne $packageMetadata) {
$installLayout = "LegacyPlatformNpm"
} else {

View File

@@ -138,10 +138,36 @@ release_metadata_url() {
printf 'https://api.github.com/repos/openai/codex/releases/tags/rust-v%s\n' "$resolved_version"
}
resolve_release() {
normalized_version="$(normalize_version "$RELEASE")"
validate_version "$normalized_version"
if [ "$normalized_version" = "latest" ]; then
requested_release="latest"
metadata_url="https://api.github.com/repos/openai/codex/releases/latest"
else
resolved_version="$normalized_version"
requested_release="$resolved_version"
metadata_url="$(release_metadata_url "$resolved_version")"
fi
if ! release_json="$(download_text "$metadata_url")"; then
echo "Could not fetch GitHub release metadata for Codex $requested_release. GitHub API may be unavailable or rate limited." >&2
exit 1
fi
if [ "$normalized_version" = "latest" ]; then
resolved_version="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"rust-v\([^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$resolved_version" ]; then
echo "Failed to resolve the latest Codex release version." >&2
exit 1
fi
validate_version "$resolved_version"
fi
}
release_asset_digest_or_empty() {
asset="$1"
resolved_version="$2"
release_json="$(download_text "$(release_metadata_url "$resolved_version")")"
digest="$(printf '%s\n' "$release_json" | awk -v asset="$asset" '
/"name":[[:space:]]*"[^"]+"/ {
@@ -190,16 +216,14 @@ release_asset_digest_or_empty() {
release_asset_exists() {
asset="$1"
resolved_version="$2"
release_asset_digest_or_empty "$asset" "$resolved_version" >/dev/null 2>&1
release_asset_digest_or_empty "$asset" >/dev/null 2>&1
}
release_asset_digest() {
asset="$1"
resolved_version="$2"
digest="$(release_asset_digest_or_empty "$asset" "$resolved_version" || true)"
digest="$(release_asset_digest_or_empty "$asset" || true)"
if [ -z "$digest" ]; then
echo "Could not find SHA-256 digest for release asset $asset." >&2
exit 1
@@ -275,27 +299,6 @@ require_command() {
fi
}
resolve_version() {
normalized_version="$(normalize_version "$RELEASE")"
validate_version "$normalized_version"
if [ "$normalized_version" != "latest" ]; then
printf '%s\n' "$normalized_version"
return
fi
release_json="$(download_text "https://api.github.com/repos/openai/codex/releases/latest")"
resolved="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"rust-v\([^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$resolved" ]; then
echo "Failed to resolve the latest Codex release version." >&2
exit 1
fi
validate_version "$resolved"
printf '%s\n' "$resolved"
}
pick_profile() {
# Use the same shell-specific split Homebrew documents because there is no
# universal startup file across macOS/Linux login and interactive shells.
@@ -839,14 +842,14 @@ else
fi
fi
resolved_version="$(resolve_version)"
resolve_release
package_asset="codex-package-$vendor_target.tar.gz"
checksum_asset="codex-package_SHA256SUMS"
if release_asset_exists "$package_asset" "$resolved_version" &&
release_asset_exists "$checksum_asset" "$resolved_version"; then
if release_asset_exists "$package_asset" &&
release_asset_exists "$checksum_asset"; then
install_layout="package"
asset="$package_asset"
elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz" "$resolved_version"; then
elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz"; then
install_layout="legacy-platform-npm"
asset="codex-npm-$npm_tag-$resolved_version.tgz"
else
@@ -893,12 +896,12 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target"
step "Downloading Codex CLI"
if [ "$install_layout" = "package" ]; then
checksum_digest="$(release_asset_digest "$checksum_asset" "$resolved_version")"
checksum_digest="$(release_asset_digest "$checksum_asset")"
download_file "$checksum_url" "$checksum_path"
verify_archive_digest "$checksum_path" "$checksum_digest"
expected_digest="$(package_archive_digest "$asset" "$checksum_path")"
else
expected_digest="$(release_asset_digest "$asset" "$resolved_version")"
expected_digest="$(release_asset_digest "$asset")"
fi
download_file "$download_url" "$archive_path"
verify_archive_digest "$archive_path" "$expected_digest"

View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python3
import json
import os
from pathlib import Path
import subprocess
import tempfile
import textwrap
import unittest
INSTALL_SCRIPT = Path(__file__).with_name("install.sh")
VERSION = "0.142.5"
class InstallShTest(unittest.TestCase):
def test_metadata_fetch_failure_is_not_reported_as_missing_assets(self) -> None:
result, requests = run_installer(VERSION, metadata_failure=True)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(
requests,
[
"https://api.github.com/repos/openai/codex/releases/tags/"
f"rust-v{VERSION}"
],
)
self.assertIn(
f"Could not fetch GitHub release metadata for Codex {VERSION}",
result.stderr,
)
self.assertNotIn("Could not find Codex package", result.stderr)
def test_exact_release_fetches_metadata_once(self) -> None:
result, requests = run_installer(VERSION)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(
requests,
[
"https://api.github.com/repos/openai/codex/releases/tags/"
f"rust-v{VERSION}",
"https://github.com/openai/codex/releases/download/"
f"rust-v{VERSION}/codex-package_SHA256SUMS",
],
)
self.assertIn(f"Resolved version: {VERSION}", result.stdout)
def test_latest_release_reuses_version_metadata(self) -> None:
result, requests = run_installer("latest")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(
requests,
[
"https://api.github.com/repos/openai/codex/releases/latest",
"https://github.com/openai/codex/releases/download/"
f"rust-v{VERSION}/codex-package_SHA256SUMS",
],
)
self.assertIn(f"Resolved version: {VERSION}", result.stdout)
def run_installer(
release: str, *, metadata_failure: bool = False
) -> tuple[subprocess.CompletedProcess[str], list[str]]:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
bin_dir = root / "bin"
bin_dir.mkdir()
request_log = root / "requests.log"
fake_curl = bin_dir / "curl"
fake_curl.write_text(
textwrap.dedent(
"""\
#!/bin/sh
url=""
for arg in "$@"; do
case "$arg" in
https://*) url="$arg" ;;
esac
done
printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG"
case "$url" in
https://api.github.com/*)
if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then
echo "curl: (22) The requested URL returned error: 403" >&2
exit 22
fi
printf '%s\n' "$CODEX_TEST_METADATA_JSON"
;;
*)
exit 22
;;
esac
"""
),
encoding="utf-8",
)
fake_curl.chmod(0o755)
env = os.environ.copy()
env.update(
{
"CODEX_HOME": str(root / "codex-home"),
"CODEX_INSTALL_DIR": str(root / "install-bin"),
"CODEX_NON_INTERACTIVE": "1",
"CODEX_RELEASE": release,
"CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0",
"CODEX_TEST_METADATA_JSON": release_metadata(),
"CODEX_TEST_REQUEST_LOG": str(request_log),
"HOME": str(root / "home"),
"PATH": f"{bin_dir}:/usr/bin:/bin",
"SHELL": "/bin/sh",
}
)
result = subprocess.run(
["/bin/sh", str(INSTALL_SCRIPT)],
capture_output=True,
check=False,
env=env,
text=True,
)
requests = (
request_log.read_text(encoding="utf-8").splitlines()
if request_log.exists()
else []
)
return result, requests
def release_metadata() -> str:
assets = [
{
"name": f"codex-package-{target}.tar.gz",
"digest": f"sha256:{'a' * 64}",
}
for target in (
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"aarch64-unknown-linux-musl",
"x86_64-unknown-linux-musl",
)
]
assets.append(
{
"name": "codex-package_SHA256SUMS",
"digest": f"sha256:{'b' * 64}",
}
)
return json.dumps(
{"tag_name": f"rust-v{VERSION}", "assets": assets},
indent=2,
)
if __name__ == "__main__":
unittest.main()