mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
3
.github/workflows/repo-checks.yml
vendored
3
.github/workflows/repo-checks.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
159
scripts/install/test_install_sh.py
Normal file
159
scripts/install/test_install_sh.py
Normal 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()
|
||||
Reference in New Issue
Block a user