From 39a2438d16514d0d6f88105d17b0f747994af487 Mon Sep 17 00:00:00 2001 From: zsol-openai Date: Thu, 23 Jul 2026 09:16:41 +0000 Subject: [PATCH] Prefer releases.openai.com in standalone installers (#34910) ## What changed - Make the shell and PowerShell installers download release metadata and assets from `releases.openai.com` by default, with bounded request timeouts. - Fall back to GitHub Releases when metadata is unusable, required assets are missing, or downloaded assets fail checksum or manifest validation. - Document `CODEX_INSTALLER_USE_RELEASES_OPENAI_COM=false` for opting out of the default release source. ## Testing - Cover default-source installation, malformed and incomplete metadata, version mismatches, corrupt assets, invalid checksums, incomplete manifests, and GitHub fallback failures in the shell installer tests. GitOrigin-RevId: 1c65d527ca79761358c3a4cd7a5fd675e1ef75b2 --- README.md | 10 + scripts/install/install.ps1 | 73 ++++--- scripts/install/install.sh | 102 +++++++--- scripts/install/test_install_sh.py | 293 +++++++++++++++++++++++++++-- 4 files changed, 416 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index bb2a9d18c3..06b02fced7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ Run the following on Windows to install Codex CLI: powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex" ``` +The standalone installers download from `https://releases.openai.com/codex` by default and fall back to GitHub Releases if a metadata or asset download is unavailable. To force GitHub Releases, set `CODEX_INSTALLER_USE_RELEASES_OPENAI_COM` to `false` (`0` and `no` are also accepted): + +```shell +curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_INSTALLER_USE_RELEASES_OPENAI_COM=false sh +``` + +```powershell +$env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM='false'; irm https://chatgpt.com/codex/install.ps1 | iex +``` + Codex CLI can also be installed via the following package managers: ```shell diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index e5c4669bd5..80d41a798a 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -12,13 +12,15 @@ if ([string]::IsNullOrWhiteSpace($Release)) { } $NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" -$DefaultPreferReleasesOpenAICom = $false +$DefaultPreferReleasesOpenAICom = $true $PreferReleasesOpenAICom = if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM)) { $DefaultPreferReleasesOpenAICom } else { $env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM -match "^(?i:1|true|yes)$" } $ReleasesBaseUri = "https://releases.openai.com/codex" +$ReleasesMetadataTimeoutSec = 30 +$ReleasesAssetTimeoutSec = 300 function Write-Step { param( @@ -111,17 +113,45 @@ function Find-ReleaseAssetMetadata { function Invoke-WebRequestWithFallback { param( [object]$Metadata, - [string]$OutFile + [string]$OutFile, + [string]$ExpectedDigest, + [string]$AssetName, + [string]$ReleaseVersion, + [string]$RequiredManifestAsset ) try { - Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile + if ($Metadata.Url.StartsWith("$ReleasesBaseUri/", [System.StringComparison]::OrdinalIgnoreCase)) { + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile -TimeoutSec $ReleasesAssetTimeoutSec + } else { + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile + } + Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest + if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { + $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset + } } catch { if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) { throw } - Write-WarningStep "Could not download $($Metadata.Url); retrying from GitHub Releases." + Write-WarningStep "Could not download or verify $($Metadata.Url); retrying from GitHub Releases." Invoke-WebRequest -UseBasicParsing -Uri $Metadata.FallbackUrl -OutFile $OutFile + try { + Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest + if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { + $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset + } + } catch { + $githubRelease = Resolve-ReleaseFromGitHub -NormalizedVersion $ReleaseVersion + $githubAssetMetadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ReleaseMetadata $githubRelease.Metadata + if ($null -eq $githubAssetMetadata) { + throw "Could not find GitHub release metadata for asset $AssetName." + } + Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $githubAssetMetadata.Sha256 + if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { + $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset + } + } } } @@ -337,25 +367,22 @@ function Resolve-ReleaseFromReleases { "$ReleasesBaseUri/releases/$NormalizedVersion/release.json" } try { - $metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri $metadataUri + $metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri $metadataUri -TimeoutSec $ReleasesMetadataTimeoutSec + $releaseMetadata = [string]$metadataResponse.Content | ConvertFrom-Json -ErrorAction Stop + $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata + if ($NormalizedVersion -ne "latest" -and $resolvedVersion -cne $NormalizedVersion) { + throw "Release metadata version did not match requested Codex version $NormalizedVersion." + } + $resolvedRelease = [PSCustomObject]@{ + Version = $resolvedVersion + Metadata = $releaseMetadata + Source = "ReleasesOpenAICom" + } + $null = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag } catch { return $null } - try { - $releaseMetadata = [string]$metadataResponse.Content | ConvertFrom-Json -ErrorAction Stop - } catch { - throw "Invalid release metadata from releases.openai.com." - } - - $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata - if ($NormalizedVersion -ne "latest" -and $resolvedVersion -cne $NormalizedVersion) { - throw "Release metadata version did not match requested Codex version $NormalizedVersion." - } - return [PSCustomObject]@{ - Version = $resolvedVersion - Metadata = $releaseMetadata - Source = "ReleasesOpenAICom" - } + return $resolvedRelease } function Resolve-Release { @@ -932,14 +959,12 @@ try { Write-Step "Downloading Codex CLI" if ($installLayout -eq "Package") { - Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath - Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256 + Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath -ExpectedDigest $checksumMetadata.Sha256 -AssetName $checksumAsset -ReleaseVersion $resolvedVersion -RequiredManifestAsset $packageAsset $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset } else { $expectedPackageDigest = $packageMetadata.Sha256 } - Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath - Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest + Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath -ExpectedDigest $expectedPackageDigest -AssetName $packageAsset -ReleaseVersion $resolvedVersion New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null if (Test-Path -LiteralPath $stagingDir) { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 7e4688881a..9c7dfabd3d 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -4,9 +4,12 @@ set -eu RELEASE="${CODEX_RELEASE:-latest}" NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" -DEFAULT_PREFER_RELEASES_OPENAI_COM="false" +DEFAULT_PREFER_RELEASES_OPENAI_COM="true" PREFER_RELEASES_OPENAI_COM="${CODEX_INSTALLER_USE_RELEASES_OPENAI_COM:-$DEFAULT_PREFER_RELEASES_OPENAI_COM}" RELEASES_BASE_URL="https://releases.openai.com/codex" +RELEASES_CONNECT_TIMEOUT=10 +RELEASES_METADATA_TIMEOUT=30 +RELEASES_ASSET_TIMEOUT=300 release_source="github" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" @@ -61,7 +64,7 @@ validate_version() { if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-alpha(\.[0-9]+){0,2}|-beta(\.[0-9]+)?)?$'; then echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]." >&2 - exit 1 + return 1 fi } @@ -83,6 +86,8 @@ Usage: install.sh [--release VERSION] Environment: CODEX_RELEASE Version to install; overridden by --release. CODEX_NON_INTERACTIVE Set to 1, true, or yes to skip prompts. + CODEX_INSTALLER_USE_RELEASES_OPENAI_COM + Set to 0, false, or no to use GitHub Releases. EOF exit 0 ;; @@ -100,12 +105,26 @@ download_file() { output="$2" if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "$output" + case "$url" in + "$RELEASES_BASE_URL"/*) + curl -fsSL --connect-timeout "$RELEASES_CONNECT_TIMEOUT" --max-time "$RELEASES_ASSET_TIMEOUT" "$url" -o "$output" + ;; + *) + curl -fsSL "$url" -o "$output" + ;; + esac return fi if command -v wget >/dev/null 2>&1; then - wget -q -O "$output" "$url" + case "$url" in + "$RELEASES_BASE_URL"/*) + wget -q -t 1 -T "$RELEASES_ASSET_TIMEOUT" -O "$output" "$url" + ;; + *) + wget -q -O "$output" "$url" + ;; + esac return fi @@ -117,12 +136,26 @@ download_text() { url="$1" if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" + case "$url" in + "$RELEASES_BASE_URL"/*) + curl -fsSL --connect-timeout "$RELEASES_CONNECT_TIMEOUT" --max-time "$RELEASES_METADATA_TIMEOUT" "$url" + ;; + *) + curl -fsSL "$url" + ;; + esac return fi if command -v wget >/dev/null 2>&1; then - wget -q -O - "$url" + case "$url" in + "$RELEASES_BASE_URL"/*) + wget -q -t 1 -T "$RELEASES_METADATA_TIMEOUT" -O - "$url" + ;; + *) + wget -q -O - "$url" + ;; + esac return fi @@ -134,8 +167,13 @@ download_file_with_fallback() { primary_url="$1" fallback_url="$2" output="$3" + expected_digest="$4" + fallback_asset="$5" + required_manifest_asset="${6:-}" - if download_file "$primary_url" "$output"; then + if download_file "$primary_url" "$output" && + verify_archive_digest "$output" "$expected_digest" && + { [ -z "$required_manifest_asset" ] || package_archive_digest "$required_manifest_asset" "$output" >/dev/null; }; then return fi @@ -143,8 +181,19 @@ download_file_with_fallback() { return 1 fi - warn "Could not download $primary_url; retrying from GitHub Releases." + warn "Could not download or verify $primary_url; retrying from GitHub Releases." download_file "$fallback_url" "$output" + if verify_archive_digest "$output" "$expected_digest" && + { [ -z "$required_manifest_asset" ] || package_archive_digest "$required_manifest_asset" "$output" >/dev/null; }; then + return + fi + + resolve_release_from_github "$resolved_version" + fallback_digest="$(release_asset_digest "$fallback_asset")" + verify_archive_digest "$output" "$fallback_digest" + if [ -n "$required_manifest_asset" ]; then + package_archive_digest "$required_manifest_asset" "$output" >/dev/null + fi } parse_release_metadata() { @@ -279,7 +328,7 @@ parse_downloaded_release_metadata() { source_name="$2" if ! release_metadata="$(printf '%s\n' "$release_json" | parse_release_metadata)"; then echo "Could not parse $source_name release metadata for Codex $requested_release." >&2 - exit 1 + return 1 fi } @@ -291,7 +340,7 @@ resolve_metadata_version() { esac if [ -z "$metadata_version" ]; then echo "Failed to resolve the latest Codex release version." >&2 - exit 1 + return 1 fi validate_version "$metadata_version" } @@ -337,11 +386,15 @@ resolve_release_from_releases() { return 1 fi - parse_downloaded_release_metadata "$requested_release" "releases.openai.com" - resolve_metadata_version + if ! parse_downloaded_release_metadata "$requested_release" "releases.openai.com"; then + return 1 + fi + if ! resolve_metadata_version; then + return 1 + fi if [ "$normalized_version" != "latest" ] && [ "$metadata_version" != "$normalized_version" ]; then echo "Release metadata version did not match requested Codex version $normalized_version." >&2 - exit 1 + return 1 fi resolved_version="$metadata_version" release_source="releases.openai.com" @@ -353,7 +406,8 @@ resolve_release() { case "$PREFER_RELEASES_OPENAI_COM" in 1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss]) - if resolve_release_from_releases "$normalized_version"; then + if resolve_release_from_releases "$normalized_version" && + select_release_assets; then return fi warn "releases.openai.com is unavailable; falling back to GitHub Releases." @@ -361,6 +415,7 @@ resolve_release() { esac resolve_release_from_github "$normalized_version" + select_release_assets } release_asset_digest_or_empty() { @@ -375,7 +430,11 @@ release_asset_digest_or_empty() { case "$digest" in sha256:????????????????????????????????????????????????????????????????) - printf '%s\n' "${digest#sha256:}" + digest="${digest#sha256:}" + case "$digest" in + *[!0-9a-fA-F]*) return 1 ;; + esac + printf '%s\n' "$digest" ;; *) return 1 @@ -416,7 +475,7 @@ select_release_assets() { asset="codex-npm-$npm_tag-$resolved_version.tgz" else echo "Could not find Codex package or platform npm release assets for Codex $resolved_version." >&2 - exit 1 + return 1 fi if [ "$release_source" = "releases.openai.com" ]; then @@ -453,7 +512,7 @@ package_archive_digest() { if [ -z "$digest" ]; then echo "Could not find SHA-256 digest for $asset in codex-package_SHA256SUMS." >&2 - exit 1 + return 1 fi printf '%s\n' "$digest" @@ -490,7 +549,7 @@ verify_archive_digest() { echo "Downloaded Codex archive checksum did not match expected digest." >&2 echo "expected: $expected_digest" >&2 echo "actual: $actual_digest" >&2 - exit 1 + return 1 fi } @@ -1066,7 +1125,6 @@ else fi resolve_release -select_release_assets release_name="$resolved_version-$vendor_target" release_dir="$RELEASES_DIR/$release_name" current_version="$(current_installed_version)" @@ -1106,14 +1164,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")" - download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" - verify_archive_digest "$checksum_path" "$checksum_digest" + download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" "$checksum_digest" "$checksum_asset" "$asset" expected_digest="$(package_archive_digest "$asset" "$checksum_path")" else expected_digest="$(release_asset_digest "$asset")" fi - download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" - verify_archive_digest "$archive_path" "$expected_digest" + download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" "$expected_digest" "$asset" step "Installing standalone package to $release_dir" if [ "$install_layout" = "package" ]; then diff --git a/scripts/install/test_install_sh.py b/scripts/install/test_install_sh.py index f7c41ee45a..02d527d608 100644 --- a/scripts/install/test_install_sh.py +++ b/scripts/install/test_install_sh.py @@ -34,8 +34,8 @@ class InstallShTest(unittest.TestCase): ) self.assertNotIn("Could not find Codex package", result.stderr) - def test_exact_release_fetches_metadata_once(self) -> None: - result, requests = run_installer(VERSION) + def test_exact_release_opt_out_uses_github_metadata_once(self) -> None: + result, requests = run_installer(VERSION, use_mirror=False) self.assertNotEqual(result.returncode, 0) self.assertEqual( @@ -131,7 +131,7 @@ class InstallShTest(unittest.TestCase): ) self.assertTrue(os.access(host_path, os.X_OK)) - def test_releases_latest_installs_verified_package(self) -> None: + def test_releases_latest_installs_verified_package_by_default(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) archive_path, checksum_path, metadata_json = create_package_release(root) @@ -143,7 +143,7 @@ class InstallShTest(unittest.TestCase): archive_path=archive_path, checksum_path=checksum_path, force_macos=True, - use_releases=True, + use_mirror=None, ) self.assertEqual(result.returncode, 0, result.stderr) @@ -156,6 +156,101 @@ class InstallShTest(unittest.TestCase): ], ) + def test_releases_unusable_metadata_falls_back_to_github(self) -> None: + unusable_metadata = { + "html": "proxy error", + "empty": "", + "malformed_json": '{"tag_name":', + "missing_tag": json.dumps({"assets": []}), + "missing_assets": json.dumps( + {"tag_name": f"rust-v{VERSION}", "assets": []} + ), + "invalid_checksum_digest": json.dumps( + { + "tag_name": f"rust-v{VERSION}", + "assets": [ + { + "name": "codex-package-aarch64-apple-darwin.tar.gz", + "digest": "sha256:" + "a" * 64, + }, + { + "name": "codex-package_SHA256SUMS", + "digest": "sha256:" + "z" * 64, + }, + ], + } + ), + "invalid_version": json.dumps({"tag_name": "rust-vinvalid"}), + } + + for name, releases_metadata_json in unusable_metadata.items(): + with self.subTest(metadata=name): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release( + root + ) + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + releases_metadata_json=releases_metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_mirror=None, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + "https://api.github.com/repos/openai/codex/releases/latest", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn("falling back to GitHub Releases", result.stderr) + + def test_releases_exact_metadata_version_mismatch_falls_back_to_github( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + releases_metadata = json.loads(metadata_json) + releases_metadata["tag_name"] = f"rust-v{MISMATCH_VERSION}" + + result, requests = run_installer_in( + root, + VERSION, + metadata_json=metadata_json, + releases_metadata_json=json.dumps(releases_metadata), + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_mirror=None, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + f"https://releases.openai.com/codex/releases/{VERSION}/release.json", + "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", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn("falling back to GitHub Releases", result.stderr) + def test_releases_asset_download_falls_back_to_github(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) @@ -168,7 +263,7 @@ class InstallShTest(unittest.TestCase): archive_path=archive_path, checksum_path=checksum_path, force_macos=True, - use_releases=True, + use_mirror=None, releases_mode="asset_fallback", ) @@ -187,6 +282,147 @@ class InstallShTest(unittest.TestCase): ) self.assertIn("retrying from GitHub Releases", result.stderr) + def test_releases_corrupt_assets_fall_back_to_github(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_mirror=None, + releases_mode="corrupt_assets", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn("checksum did not match expected digest", result.stderr) + self.assertIn("retrying from GitHub Releases", result.stderr) + + def test_releases_wrong_checksum_digest_uses_github_metadata(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + mirror_metadata = json.loads(metadata_json) + for release_asset in mirror_metadata["assets"]: + if release_asset["name"] == "codex-package_SHA256SUMS": + release_asset["digest"] = "sha256:" + "0" * 64 + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + releases_metadata_json=json.dumps(mirror_metadata), + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_mirror=None, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn("checksum did not match expected digest", result.stderr) + + def test_releases_incomplete_checksum_manifest_falls_back_to_github(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + mirror_checksum_path = root / "mirror-SHA256SUMS" + mirror_checksum_path.write_text( + f"{'a' * 64} codex-package-other-platform.tar.gz\n", + encoding="utf-8", + ) + mirror_metadata = json.loads(metadata_json) + for release_asset in mirror_metadata["assets"]: + if release_asset["name"] == "codex-package_SHA256SUMS": + release_asset["digest"] = ( + "sha256:" + + hashlib.sha256(mirror_checksum_path.read_bytes()).hexdigest() + ) + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + releases_metadata_json=json.dumps(mirror_metadata), + archive_path=archive_path, + checksum_path=checksum_path, + releases_checksum_path=mirror_checksum_path, + force_macos=True, + use_mirror=None, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn("retrying from GitHub Releases", result.stderr) + + def test_releases_corrupt_github_fallback_still_fails(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_mirror=None, + releases_mode="corrupt_checksum_and_github", + ) + + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}", + ], + ) + self.assertIn("checksum did not match expected digest", result.stderr) + def test_releases_exact_rejects_wrong_binary_version(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) @@ -202,7 +438,7 @@ class InstallShTest(unittest.TestCase): archive_path=archive_path, checksum_path=checksum_path, force_macos=True, - use_releases=True, + use_mirror=True, ) self.assertNotEqual(result.returncode, 0) @@ -231,7 +467,7 @@ class InstallShTest(unittest.TestCase): metadata_json=metadata_json, legacy_archive_path=archive_path, force_macos=True, - use_releases=True, + use_mirror=True, releases_mode="channel_failure", ) @@ -253,7 +489,7 @@ class InstallShTest(unittest.TestCase): VERSION, metadata_json=metadata_json, force_macos=True, - use_releases=True, + use_mirror=True, releases_mode="channel_failure", ) @@ -274,6 +510,7 @@ def run_installer( *, metadata_failure: bool = False, metadata_json: str | None = None, + use_mirror: bool | None = False, ) -> tuple[subprocess.CompletedProcess[str], list[str]]: with tempfile.TemporaryDirectory() as temp_dir: return run_installer_in( @@ -281,6 +518,7 @@ def run_installer( release, metadata_failure=metadata_failure, metadata_json=metadata_json, + use_mirror=use_mirror, ) @@ -290,11 +528,13 @@ def run_installer_in( *, metadata_failure: bool = False, metadata_json: str | None = None, + releases_metadata_json: str | None = None, archive_path: Path | None = None, checksum_path: Path | None = None, + releases_checksum_path: Path | None = None, legacy_archive_path: Path | None = None, force_macos: bool = False, - use_releases: bool = False, + use_mirror: bool | None = False, releases_mode: str = "", ) -> tuple[subprocess.CompletedProcess[str], list[str]]: bin_dir = root / "bin" @@ -337,8 +577,13 @@ def run_installer_in( if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then exit 22 fi - if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then - cp "$CODEX_TEST_CHECKSUM_PATH" "$output" + if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ] || + [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then + printf 'proxy error\n' >"$output" + exit 0 + fi + if [ -n "$CODEX_TEST_RELEASES_CHECKSUM_PATH" ]; then + cp "$CODEX_TEST_RELEASES_CHECKSUM_PATH" "$output" else exit 22 fi @@ -347,6 +592,10 @@ def run_installer_in( if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then exit 22 fi + if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ]; then + printf 'proxy error\n' >"$output" + exit 0 + fi if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then cp "$CODEX_TEST_ARCHIVE_PATH" "$output" else @@ -354,6 +603,10 @@ def run_installer_in( fi ;; https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS) + if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then + printf 'proxy error\n' >"$output" + exit 0 + fi if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then cp "$CODEX_TEST_CHECKSUM_PATH" "$output" else @@ -404,18 +657,22 @@ def run_installer_in( "CODEX_INSTALL_DIR": str(root / "install-bin"), "CODEX_NON_INTERACTIVE": "1", "CODEX_RELEASE": release, - "CODEX_INSTALLER_USE_RELEASES_OPENAI_COM": ( - "TRUE" if use_releases else "0" - ), "CODEX_TEST_ARCHIVE_PATH": str(archive_path or ""), "CODEX_TEST_CHECKSUM_PATH": str(checksum_path or ""), + "CODEX_TEST_RELEASES_CHECKSUM_PATH": str( + releases_checksum_path or checksum_path or "" + ), "CODEX_TEST_LEGACY_ARCHIVE_PATH": str(legacy_archive_path or ""), "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", "CODEX_TEST_METADATA_JSON": ( metadata_json if metadata_json is not None else release_metadata() ), "CODEX_TEST_RELEASES_METADATA_JSON": ( - metadata_json if metadata_json is not None else release_metadata() + releases_metadata_json + if releases_metadata_json is not None + else metadata_json + if metadata_json is not None + else release_metadata() ), "CODEX_TEST_RELEASES_MODE": releases_mode, "CODEX_TEST_REQUEST_LOG": str(request_log), @@ -424,6 +681,12 @@ def run_installer_in( "SHELL": "/bin/sh", } ) + if use_mirror is None: + env.pop("CODEX_INSTALLER_USE_RELEASES_OPENAI_COM", None) + else: + env["CODEX_INSTALLER_USE_RELEASES_OPENAI_COM"] = ( + "TRUE" if use_mirror else "false" + ) result = subprocess.run( ["/bin/sh", str(INSTALL_SCRIPT)], capture_output=True,