From 01be10e61fdd5f84870509e5db0059a043db2ca2 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Wed, 15 Jul 2026 17:27:51 +0200 Subject: [PATCH] Prefer releases.openai.com for Codex installs --- scripts/install/install.ps1 | 255 ++++++++++++++++++++---- scripts/install/install.sh | 182 +++++++++++++---- scripts/install/test_install_sh.py | 306 ++++++++++++++++++++++++++++- 3 files changed, 663 insertions(+), 80 deletions(-) diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index e27d34b223..4991b135ad 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -12,6 +12,13 @@ if ([string]::IsNullOrWhiteSpace($Release)) { } $NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" +$DefaultPreferReleasesOpenAICom = $false +$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" function Write-Step { param( @@ -79,7 +86,8 @@ function Assert-ValidReleaseVersion { function Find-ReleaseAssetMetadata { param( [string]$AssetName, - [object]$ReleaseMetadata + [object]$ReleaseMetadata, + [string]$FallbackUrl = $null ) $asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 @@ -94,10 +102,112 @@ function Find-ReleaseAssetMetadata { return [PSCustomObject]@{ Url = $asset.browser_download_url + FallbackUrl = $FallbackUrl Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant() } } +function New-ReleasesAssetMetadata { + param( + [string]$AssetName, + [string]$Version + ) + + return [PSCustomObject]@{ + Url = "$ReleasesBaseUri/releases/$Version/$AssetName" + FallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$Version/$AssetName" + Sha256 = $null + } +} + +function Invoke-WebRequestWithFallback { + param( + [object]$Metadata, + [string]$OutFile, + [switch]$AllowFailure + ) + + try { + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile + } catch { + if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) { + if ($AllowFailure) { + return $false + } + throw + } + Write-WarningStep "Could not download $($Metadata.Url); retrying from GitHub Releases." + try { + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.FallbackUrl -OutFile $OutFile + } catch { + if ($AllowFailure) { + return $false + } + throw + } + } + + return $true +} + +function Resolve-ReleaseAssetSelection { + param( + [object]$ResolvedRelease, + [string]$Target, + [string]$NpmTag + ) + + $version = $ResolvedRelease.Version + $releaseMetadata = $ResolvedRelease.Metadata + $packageAsset = "codex-package-$Target.tar.gz" + $checksumAsset = "codex-package_SHA256SUMS" + + if ($ResolvedRelease.Source -eq "ReleasesOpenAICom" -and $null -eq $releaseMetadata) { + return [PSCustomObject]@{ + PackageAsset = $packageAsset + PackageMetadata = New-ReleasesAssetMetadata -AssetName $packageAsset -Version $version + ChecksumMetadata = New-ReleasesAssetMetadata -AssetName $checksumAsset -Version $version + InstallLayout = "Package" + } + } + + $packageFallbackUrl = $null + $checksumFallbackUrl = $null + if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { + $packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" + $checksumFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$checksumAsset" + } + + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -FallbackUrl $packageFallbackUrl + $checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata -FallbackUrl $checksumFallbackUrl + if ($null -ne $packageMetadata -and $null -ne $checksumMetadata) { + return [PSCustomObject]@{ + PackageAsset = $packageAsset + PackageMetadata = $packageMetadata + ChecksumMetadata = $checksumMetadata + InstallLayout = "Package" + } + } + + $packageAsset = "codex-npm-$NpmTag-$version.tgz" + $packageFallbackUrl = if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { + "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" + } else { + $null + } + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -FallbackUrl $packageFallbackUrl + if ($null -eq $packageMetadata) { + throw "Could not find Codex package or platform npm release assets for Codex $version." + } + + return [PSCustomObject]@{ + PackageAsset = $packageAsset + PackageMetadata = $packageMetadata + ChecksumMetadata = $null + InstallLayout = "LegacyPlatformNpm" + } +} + function Test-ArchiveDigest { param( [string]$ArchivePath, @@ -201,15 +311,30 @@ function Remove-StaleInstallArtifacts { } } -function Resolve-Release { - $normalizedVersion = Normalize-Version -RawVersion $Release - Assert-ValidReleaseVersion -Version $normalizedVersion +function Resolve-VersionFromReleaseMetadata { + param( + [object]$ReleaseMetadata + ) - 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 $resolvedVersion +} + +function Resolve-ReleaseFromGitHub { + param( + [string]$NormalizedVersion + ) + + if ($NormalizedVersion -eq "latest") { $requestedRelease = "latest" $metadataUri = "https://api.github.com/repos/openai/codex/releases/latest" } else { - $resolvedVersion = $normalizedVersion + $resolvedVersion = $NormalizedVersion $requestedRelease = $resolvedVersion $metadataUri = "https://api.github.com/repos/openai/codex/releases/tags/rust-v$resolvedVersion" } @@ -220,21 +345,61 @@ function Resolve-Release { throw "Could not fetch GitHub release metadata for Codex $requestedRelease. GitHub API may be unavailable or rate limited. $($_.Exception.Message)" } - 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 + if ($NormalizedVersion -eq "latest") { + $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata } return [PSCustomObject]@{ Version = $resolvedVersion Metadata = $releaseMetadata + Source = "GitHub" } } +function Resolve-ReleaseFromReleases { + param( + [string]$NormalizedVersion + ) + + if ($NormalizedVersion -eq "latest") { + try { + $channelResponse = Invoke-WebRequest -UseBasicParsing -Uri "$ReleasesBaseUri/channels/latest" + } catch { + return $null + } + try { + $releaseMetadata = [string]$channelResponse.Content | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "Invalid release metadata from releases.openai.com." + } + $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata + } else { + $resolvedVersion = $NormalizedVersion + $releaseMetadata = $null + } + + return [PSCustomObject]@{ + Version = $resolvedVersion + Metadata = $releaseMetadata + Source = "ReleasesOpenAICom" + } +} + +function Resolve-Release { + $normalizedVersion = Normalize-Version -RawVersion $Release + Assert-ValidReleaseVersion -Version $normalizedVersion + + if ($PreferReleasesOpenAICom) { + $release = Resolve-ReleaseFromReleases -NormalizedVersion $normalizedVersion + if ($null -ne $release) { + return $release + } + Write-WarningStep "releases.openai.com is unavailable; falling back to GitHub Releases." + } + + return Resolve-ReleaseFromGitHub -NormalizedVersion $normalizedVersion +} + function Get-VersionFromBinary { param( [string]$CodexPath @@ -767,21 +932,12 @@ Write-Step "Resolved version: $resolvedVersion" $conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir $oldStandaloneBackup = $null -$packageAsset = "codex-package-$target.tar.gz" $checksumAsset = "codex-package_SHA256SUMS" -$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 -ReleaseMetadata $releaseMetadata - if ($null -ne $packageMetadata) { - $installLayout = "LegacyPlatformNpm" - } else { - throw "Could not find Codex package or platform npm release assets for Codex $resolvedVersion." - } - $checksumMetadata = $null -} +$assetSelection = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag +$packageAsset = $assetSelection.PackageAsset +$packageMetadata = $assetSelection.PackageMetadata +$checksumMetadata = $assetSelection.ChecksumMetadata +$installLayout = $assetSelection.InstallLayout $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Force -Path $tempDir | Out-Null @@ -794,20 +950,45 @@ try { Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling." } - $archivePath = Join-Path $tempDir $packageAsset - $checksumPath = Join-Path $tempDir $checksumAsset $stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID" Write-Step "Downloading Codex CLI" - if ($installLayout -eq "Package") { - Invoke-WebRequest -Uri $checksumMetadata.Url -OutFile $checksumPath - Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256 - $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset - } else { - $expectedPackageDigest = $packageMetadata.Sha256 + $canDiscoverLegacyAsset = $resolvedRelease.Source -eq "ReleasesOpenAICom" -and $null -eq $releaseMetadata + while ($true) { + $archivePath = Join-Path $tempDir $packageAsset + $checksumPath = Join-Path $tempDir $checksumAsset + + if ($installLayout -eq "Package") { + $downloaded = Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath -AllowFailure:$canDiscoverLegacyAsset + if ($downloaded -and $null -ne $checksumMetadata.Sha256) { + Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256 + } + if ($downloaded) { + $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset + } + } else { + $downloaded = $true + $expectedPackageDigest = $packageMetadata.Sha256 + } + + if ($downloaded) { + $downloaded = Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath -AllowFailure:$canDiscoverLegacyAsset + } + if ($downloaded) { + Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest + break + } + + Write-WarningStep "The package layout is unavailable for Codex $resolvedVersion; checking GitHub release metadata for a legacy asset." + $resolvedRelease = Resolve-ReleaseFromGitHub -NormalizedVersion $resolvedVersion + $releaseMetadata = $resolvedRelease.Metadata + $assetSelection = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag + $packageAsset = $assetSelection.PackageAsset + $packageMetadata = $assetSelection.PackageMetadata + $checksumMetadata = $assetSelection.ChecksumMetadata + $installLayout = $assetSelection.InstallLayout + $canDiscoverLegacyAsset = $false } - Invoke-WebRequest -Uri $packageMetadata.Url -OutFile $archivePath - Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest 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 12b66f958a..848764f545 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -4,6 +4,11 @@ set -eu RELEASE="${CODEX_RELEASE:-latest}" NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" +DEFAULT_PREFER_RELEASES_OPENAI_COM="false" +PREFER_RELEASES_OPENAI_COM="${CODEX_INSTALLER_USE_RELEASES_OPENAI_COM:-$DEFAULT_PREFER_RELEASES_OPENAI_COM}" +RELEASES_BASE_URL="https://releases.openai.com/codex" +release_json="" +release_metadata_available="false" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" @@ -125,6 +130,23 @@ download_text() { exit 1 } +download_file_with_fallback() { + primary_url="$1" + fallback_url="$2" + output="$3" + + if download_file "$primary_url" "$output"; then + return + fi + + if [ -z "$fallback_url" ]; then + return 1 + fi + + warn "Could not download $primary_url; retrying from GitHub Releases." + download_file "$fallback_url" "$output" +} + release_url_for_asset() { asset="$1" resolved_version="$2" @@ -132,16 +154,31 @@ release_url_for_asset() { printf 'https://github.com/openai/codex/releases/download/rust-v%s/%s\n' "$resolved_version" "$asset" } +releases_url_for_asset() { + asset="$1" + resolved_version="$2" + + printf '%s/releases/%s/%s\n' "$RELEASES_BASE_URL" "$resolved_version" "$asset" +} + release_metadata_url() { resolved_version="$1" 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" +resolve_version_from_release_json() { + release_json="$1" + 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" +} +resolve_release_from_github() { + normalized_version="$1" if [ "$normalized_version" = "latest" ]; then requested_release="latest" metadata_url="https://api.github.com/repos/openai/codex/releases/latest" @@ -155,17 +192,50 @@ resolve_release() { echo "Could not fetch GitHub release metadata for Codex $requested_release. GitHub API may be unavailable or rate limited." >&2 exit 1 fi + release_metadata_available="true" + release_source="github" 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" + resolve_version_from_release_json "$release_json" fi } +resolve_release_from_releases() { + normalized_version="$1" + + if [ "$normalized_version" = "latest" ]; then + if ! release_json="$(download_text "$RELEASES_BASE_URL/channels/latest")"; then + return 1 + fi + release_metadata_available="true" + resolve_version_from_release_json "$release_json" + else + resolved_version="$normalized_version" + release_json="" + release_metadata_available="false" + fi + + requested_release="$resolved_version" + release_source="releases.openai.com" +} + +resolve_release() { + normalized_version="$(normalize_version "$RELEASE")" + validate_version "$normalized_version" + release_source="github" + + case "$PREFER_RELEASES_OPENAI_COM" in + 1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss]) + if resolve_release_from_releases "$normalized_version"; then + return + fi + warn "releases.openai.com is unavailable; falling back to GitHub Releases." + ;; + esac + + resolve_release_from_github "$normalized_version" +} + release_asset_digest_or_empty() { asset="$1" @@ -232,6 +302,43 @@ release_asset_digest() { printf '%s\n' "$digest" } +select_release_assets() { + package_asset="codex-package-$vendor_target.tar.gz" + checksum_asset="codex-package_SHA256SUMS" + download_fallback_url="" + checksum_fallback_url="" + + if [ "$release_source" = "releases.openai.com" ] && + [ "$release_metadata_available" = "false" ]; then + install_layout="package" + asset="$package_asset" + elif 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"; then + install_layout="legacy-platform-npm" + 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 + fi + + if [ "$release_source" = "releases.openai.com" ]; then + download_url="$(releases_url_for_asset "$asset" "$resolved_version")" + download_fallback_url="$(release_url_for_asset "$asset" "$resolved_version")" + if [ "$install_layout" = "package" ]; then + checksum_url="$(releases_url_for_asset "$checksum_asset" "$resolved_version")" + checksum_fallback_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")" + fi + else + download_url="$(release_url_for_asset "$asset" "$resolved_version")" + if [ "$install_layout" = "package" ]; then + checksum_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")" + fi + fi +} + package_archive_digest() { asset="$1" manifest_path="$2" @@ -292,6 +399,25 @@ verify_archive_digest() { fi } +download_selected_release() { + archive_path="$tmp_dir/$asset" + checksum_path="$tmp_dir/$checksum_asset" + + if [ "$install_layout" = "package" ]; then + download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" || return 1 + if [ "$release_metadata_available" = "true" ]; then + checksum_digest="$(release_asset_digest "$checksum_asset")" + verify_archive_digest "$checksum_path" "$checksum_digest" + fi + 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" || return 1 + verify_archive_digest "$archive_path" "$expected_digest" +} + require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "$1 is required to install Codex." >&2 @@ -847,21 +973,7 @@ else fi resolve_release -package_asset="codex-package-$vendor_target.tar.gz" -checksum_asset="codex-package_SHA256SUMS" -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"; then - install_layout="legacy-platform-npm" - 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 -fi -download_url="$(release_url_for_asset "$asset" "$resolved_version")" -checksum_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")" +select_release_assets release_name="$resolved_version-$vendor_target" release_dir="$RELEASES_DIR/$release_name" current_version="$(current_installed_version)" @@ -895,20 +1007,18 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" warn "Found incomplete existing release at $release_dir; reinstalling." fi - archive_path="$tmp_dir/$asset" - checksum_path="$tmp_dir/$checksum_asset" - step "Downloading Codex CLI" - if [ "$install_layout" = "package" ]; then - 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")" + if ! download_selected_release; then + if [ "$release_source" != "releases.openai.com" ] || + [ "$release_metadata_available" != "false" ]; then + exit 1 + fi + + warn "The package layout is unavailable for Codex $resolved_version; checking GitHub release metadata for a legacy asset." + resolve_release_from_github "$resolved_version" + select_release_assets + download_selected_release fi - download_file "$download_url" "$archive_path" - verify_archive_digest "$archive_path" "$expected_digest" 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 556c6213bb..d8c7f06978 100644 --- a/scripts/install/test_install_sh.py +++ b/scripts/install/test_install_sh.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 +import hashlib +import io import json import os from pathlib import Path import subprocess +import tarfile import tempfile import textwrap import unittest @@ -11,6 +14,7 @@ import unittest INSTALL_SCRIPT = Path(__file__).with_name("install.sh") VERSION = "0.142.5" +LEGACY_VERSION = "0.125.0" class InstallShTest(unittest.TestCase): @@ -60,14 +64,119 @@ class InstallShTest(unittest.TestCase): ) self.assertIn(f"Resolved version: {VERSION}", result.stdout) + def test_releases_channel_unavailable_falls_back_to_github(self) -> None: + result, requests = run_installer( + "latest", use_releases=True, releases_mode="channel_failure" + ) + + self.assertNotEqual(result.returncode, 0) + 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", + ], + ) + self.assertIn("falling back to GitHub Releases", result.stderr) + + def test_releases_integrity_failure_does_not_fall_back(self) -> None: + result, requests = run_installer( + "latest", use_releases=True, releases_mode="corrupt_package" + ) + + self.assertNotEqual(result.returncode, 0) + package = f"codex-package-{current_target()}.tar.gz" + self.assertEqual( + requests, + [ + "https://releases.openai.com/codex/channels/latest", + f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", + f"https://releases.openai.com/codex/releases/{VERSION}/{package}", + ], + ) + self.assertIn("checksum did not match", result.stderr) + + def test_releases_latest_installs_verified_package(self) -> None: + result, requests = run_installer("latest", use_releases=True) + + package = f"codex-package-{current_target()}.tar.gz" + 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", + f"https://releases.openai.com/codex/releases/{VERSION}/{package}", + ], + ) + + def test_releases_latest_rejects_corrupt_checksum_manifest(self) -> None: + result, requests = run_installer( + "latest", use_releases=True, releases_mode="corrupt_manifest" + ) + + 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", + ], + ) + self.assertIn("checksum did not match", result.stderr) + + def test_exact_releases_path_discovers_legacy_github_asset(self) -> None: + result, requests = run_installer( + LEGACY_VERSION, use_releases=True, releases_mode="legacy" + ) + + target = current_target() + package = f"codex-package-{target}.tar.gz" + legacy_package = f"codex-npm-{current_npm_tag()}-{LEGACY_VERSION}.tgz" + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + requests, + [ + f"https://releases.openai.com/codex/releases/{LEGACY_VERSION}/codex-package_SHA256SUMS", + f"https://releases.openai.com/codex/releases/{LEGACY_VERSION}/{package}", + "https://github.com/openai/codex/releases/download/" + f"rust-v{LEGACY_VERSION}/{package}", + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{LEGACY_VERSION}", + "https://github.com/openai/codex/releases/download/" + f"rust-v{LEGACY_VERSION}/{legacy_package}", + ], + ) + self.assertIn("retrying from GitHub Releases", result.stderr) + self.assertIn("checking GitHub release metadata", result.stderr) + def run_installer( - release: str, *, metadata_failure: bool = False + release: str, + *, + metadata_failure: bool = False, + use_releases: bool = False, + releases_mode: str = "", ) -> tuple[subprocess.CompletedProcess[str], list[str]]: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) bin_dir = root / "bin" bin_dir.mkdir() + (root / "home").mkdir() + package_archive = root / "package.tar.gz" + legacy_archive = root / "legacy.tgz" + create_package_archive(package_archive, VERSION) + create_legacy_archive(legacy_archive, LEGACY_VERSION) + package_digest = sha256_file(package_archive) + legacy_digest = sha256_file(legacy_archive) + checksum_file = root / "codex-package_SHA256SUMS" + checksum_file.write_text( + checksum_manifest(package_digest), + encoding="utf-8", + ) + checksum_digest = sha256_file(checksum_file) request_log = root / "requests.log" fake_curl = bin_dir / "curl" fake_curl.write_text( @@ -75,10 +184,18 @@ def run_installer( """\ #!/bin/sh url="" + output="" + next_is_output="false" for arg in "$@"; do case "$arg" in https://*) url="$arg" ;; esac + if [ "$next_is_output" = "true" ]; then + output="$arg" + next_is_output="false" + elif [ "$arg" = "-o" ]; then + next_is_output="true" + fi done printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG" @@ -88,7 +205,43 @@ def run_installer( echo "curl: (22) The requested URL returned error: 403" >&2 exit 22 fi - printf '%s\n' "$CODEX_TEST_METADATA_JSON" + if [ "$CODEX_TEST_RELEASES_MODE" = "legacy" ]; then + printf '%s\n' "$CODEX_TEST_LEGACY_METADATA_JSON" + else + printf '%s\n' "$CODEX_TEST_GITHUB_METADATA_JSON" + fi + ;; + https://releases.openai.com/codex/channels/latest) + if [ "$CODEX_TEST_RELEASES_MODE" = "channel_failure" ]; then + exit 22 + fi + printf '%s\n' "$CODEX_TEST_LATEST_METADATA_JSON" + ;; + https://releases.openai.com/codex/releases/*/codex-package_SHA256SUMS) + if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_manifest" ]; then + printf 'corrupt' >"$output" + else + cp "$CODEX_TEST_CHECKSUM_FILE" "$output" + fi + ;; + https://releases.openai.com/codex/releases/*/codex-package-*.tar.gz) + if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_package" ]; then + printf 'corrupt' >"$output" + exit 0 + fi + if [ "$CODEX_TEST_RELEASES_MODE" = "legacy" ]; then + exit 22 + fi + cp "$CODEX_TEST_PACKAGE_ARCHIVE" "$output" + ;; + https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS) + exit 22 + ;; + https://github.com/openai/codex/releases/download/*/codex-package-*.tar.gz) + exit 22 + ;; + https://github.com/openai/codex/releases/download/*/codex-npm-*.tgz) + cp "$CODEX_TEST_LEGACY_ARCHIVE" "$output" ;; *) exit 22 @@ -107,9 +260,31 @@ def run_installer( "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_CHECKSUM_FILE": str(checksum_file), + "CODEX_TEST_GITHUB_METADATA_JSON": release_metadata( + VERSION, + package_digest, + checksum_digest, + "https://github.com/openai/codex/releases/download", + ), + "CODEX_TEST_LEGACY_ARCHIVE": str(legacy_archive), + "CODEX_TEST_LEGACY_METADATA_JSON": legacy_release_metadata( + LEGACY_VERSION, + legacy_digest, + ), "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", - "CODEX_TEST_METADATA_JSON": release_metadata(), + "CODEX_TEST_PACKAGE_ARCHIVE": str(package_archive), + "CODEX_TEST_RELEASES_MODE": releases_mode, "CODEX_TEST_REQUEST_LOG": str(request_log), + "CODEX_TEST_LATEST_METADATA_JSON": release_metadata( + VERSION, + package_digest, + checksum_digest, + "https://releases.openai.com/codex/releases", + ), "HOME": str(root / "home"), "PATH": f"{bin_dir}:/usr/bin:/bin", "SHELL": "/bin/sh", @@ -130,11 +305,19 @@ def run_installer( return result, requests -def release_metadata() -> str: +def release_metadata( + version: str, + package_digest: str, + checksum_digest: str, + download_base_url: str, +) -> str: + release_path = f"rust-v{version}" if "github.com" in download_base_url else version + asset_base_url = f"{download_base_url}/{release_path}" assets = [ { "name": f"codex-package-{target}.tar.gz", - "digest": f"sha256:{'a' * 64}", + "digest": f"sha256:{package_digest}", + "browser_download_url": f"{asset_base_url}/codex-package-{target}.tar.gz", } for target in ( "aarch64-apple-darwin", @@ -146,14 +329,123 @@ def release_metadata() -> str: assets.append( { "name": "codex-package_SHA256SUMS", - "digest": f"sha256:{'b' * 64}", + "digest": f"sha256:{checksum_digest}", + "browser_download_url": f"{asset_base_url}/codex-package_SHA256SUMS", } ) return json.dumps( - {"tag_name": f"rust-v{VERSION}", "assets": assets}, + {"tag_name": f"rust-v{version}", "assets": assets}, indent=2, ) +def legacy_release_metadata(version: str, digest: str) -> str: + asset = f"codex-npm-{current_npm_tag()}-{version}.tgz" + return json.dumps( + { + "tag_name": f"rust-v{version}", + "assets": [ + { + "name": asset, + "digest": f"sha256:{digest}", + "browser_download_url": ( + "https://github.com/openai/codex/releases/download/" + f"rust-v{version}/{asset}" + ), + } + ], + }, + indent=2, + ) + + +def current_target() -> str: + os_name = subprocess.run( + ["uname", "-s"], capture_output=True, check=True, text=True + ).stdout.strip() + architecture = subprocess.run( + ["uname", "-m"], capture_output=True, check=True, text=True + ).stdout.strip() + arch = "aarch64" if architecture in ("arm64", "aarch64") else "x86_64" + return ( + f"{arch}-apple-darwin" if os_name == "Darwin" else f"{arch}-unknown-linux-musl" + ) + + +def current_npm_tag() -> str: + target = current_target() + return { + "aarch64-apple-darwin": "darwin-arm64", + "x86_64-apple-darwin": "darwin-x64", + "aarch64-unknown-linux-musl": "linux-arm64", + "x86_64-unknown-linux-musl": "linux-x64", + }[target] + + +def checksum_manifest(package_digest: str) -> str: + return "".join( + f"{package_digest} codex-package-{target}.tar.gz\n" + for target in ( + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-unknown-linux-musl", + "x86_64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "x86_64-pc-windows-msvc", + ) + ) + + +def create_package_archive(path: Path, version: str) -> None: + executable = f"#!/bin/sh\necho 'codex-cli {version}'\n".encode() + with tarfile.open(path, "w:gz") as archive: + add_archive_file(archive, "codex-package.json", b"{}\n") + add_archive_file(archive, "bin/codex", executable, mode=0o755) + add_archive_file(archive, "bin/codex-code-mode-host", executable, mode=0o755) + add_archive_file(archive, "codex-path/rg", executable, mode=0o755) + add_archive_file(archive, "codex-resources/bwrap", executable, mode=0o755) + + +def create_legacy_archive(path: Path, version: str) -> None: + target = current_target() + executable = f"#!/bin/sh\necho 'codex-cli {version}'\n".encode() + with tarfile.open(path, "w:gz") as archive: + add_archive_file( + archive, + f"package/vendor/{target}/codex/codex", + executable, + mode=0o755, + ) + add_archive_file( + archive, + f"package/vendor/{target}/path/rg", + executable, + mode=0o755, + ) + add_archive_file( + archive, + f"package/vendor/{target}/codex-resources/bwrap", + executable, + mode=0o755, + ) + + +def add_archive_file( + archive: tarfile.TarFile, + name: str, + content: bytes, + *, + mode: int = 0o644, +) -> None: + info = tarfile.TarInfo(name) + info.size = len(content) + info.mode = mode + archive.addfile(info, io.BytesIO(content)) + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + if __name__ == "__main__": unittest.main()