From 765675a122c9d1d4add7ce53d48c2d416fae39e9 Mon Sep 17 00:00:00 2001 From: zsol-openai Date: Tue, 21 Jul 2026 10:34:10 +0000 Subject: [PATCH] Add an optional releases.openai.com installer source (#34514) ## What changed - Let the shell and PowerShell installers resolve metadata and download assets from `releases.openai.com` when `CODEX_INSTALLER_USE_RELEASES_OPENAI_COM` is enabled. - Fall back to GitHub Releases when metadata or asset downloads from the alternate source are unavailable. - Verify that an installed or cached Codex binary reports the resolved version before activating it. ## Testing - Cover latest-release installation, asset fallback, mismatched binary versions, and reuse of a cached legacy package in the shell installer tests. GitOrigin-RevId: 48c57d3377fbc6b0903570c665041bb465ca6f4c --- scripts/install/install.ps1 | 198 +++++++++++++++++++++---- scripts/install/install.sh | 179 ++++++++++++++++++----- scripts/install/test_install_sh.py | 225 ++++++++++++++++++++++++++++- 3 files changed, 529 insertions(+), 73 deletions(-) diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 0c12ba0a29..e5c4669bd5 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,9 @@ function Assert-ValidReleaseVersion { function Find-ReleaseAssetMetadata { param( [string]$AssetName, - [object]$ReleaseMetadata + [object]$ReleaseMetadata, + [string]$Url = $null, + [string]$FallbackUrl = $null ) $asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 @@ -93,11 +102,82 @@ function Find-ReleaseAssetMetadata { } return [PSCustomObject]@{ - Url = $asset.browser_download_url + Url = if ([string]::IsNullOrWhiteSpace($Url)) { $asset.browser_download_url } else { $Url } + FallbackUrl = $FallbackUrl Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant() } } +function Invoke-WebRequestWithFallback { + param( + [object]$Metadata, + [string]$OutFile + ) + + try { + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile + } catch { + if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) { + throw + } + Write-WarningStep "Could not download $($Metadata.Url); retrying from GitHub Releases." + Invoke-WebRequest -UseBasicParsing -Uri $Metadata.FallbackUrl -OutFile $OutFile + } +} + +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" + $packageUrl = $null + $packageFallbackUrl = $null + $checksumUrl = $null + $checksumFallbackUrl = $null + if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { + $packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset" + $packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" + $checksumUrl = "$ReleasesBaseUri/releases/$version/$checksumAsset" + $checksumFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$checksumAsset" + } + + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -FallbackUrl $packageFallbackUrl + $checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata -Url $checksumUrl -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" + $packageUrl = $null + $packageFallbackUrl = $null + if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { + $packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset" + $packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" + } + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -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 +281,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 +315,64 @@ 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 + ) + + $metadataUri = if ($NormalizedVersion -eq "latest") { + "$ReleasesBaseUri/channels/latest" + } else { + "$ReleasesBaseUri/releases/$NormalizedVersion/release.json" + } + try { + $metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri $metadataUri + } 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" + } +} + +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 @@ -589,18 +727,21 @@ function Test-ReleaseIsComplete { if (-not (Test-PackageContentsAreComplete -PackageDir $ReleaseDir)) { return $false } + $codexPath = Join-Path $ReleaseDir "bin\codex.exe" } "LegacyPlatformNpm" { if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $ReleaseDir)) { return $false } + $codexPath = Join-Path $ReleaseDir "codex.exe" } default { throw "Unknown Codex installer layout: $Layout" } } - return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget" + return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget" -and + (Get-VersionFromBinary -CodexPath $codexPath) -ceq $ExpectedVersion } function Get-ExistingCodexCommand { @@ -767,21 +908,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 @@ -800,13 +932,13 @@ try { Write-Step "Downloading Codex CLI" if ($installLayout -eq "Package") { - Invoke-WebRequest -Uri $checksumMetadata.Url -OutFile $checksumPath + Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256 $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset } else { $expectedPackageDigest = $packageMetadata.Sha256 } - Invoke-WebRequest -Uri $packageMetadata.Url -OutFile $archivePath + Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null @@ -849,6 +981,10 @@ try { Move-Item -LiteralPath $stagingDir -Destination $releaseDir } + if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) { + throw "Installed Codex command did not report expected version $resolvedVersion." + } + New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 8d2d08822d..7e4688881a 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -4,6 +4,10 @@ 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_source="github" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" @@ -126,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" +} + parse_release_metadata() { # Bound awk's record size so compact, single-line JSON stays fast on every # supported awk implementation. JSON strings cannot contain literal newlines, @@ -240,16 +261,43 @@ 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" +parse_downloaded_release_metadata() { + requested_release="$1" + 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 + fi +} +resolve_metadata_version() { + release_tag="$(printf '%s\n' "$release_metadata" | awk -F '\t' '$1 == "tag_name" { print $2; exit }')" + case "$release_tag" in + rust-v*) metadata_version="${release_tag#rust-v}" ;; + *) metadata_version="" ;; + esac + if [ -z "$metadata_version" ]; then + echo "Failed to resolve the latest Codex release version." >&2 + exit 1 + fi + validate_version "$metadata_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" @@ -264,23 +312,55 @@ resolve_release() { exit 1 fi - if ! release_metadata="$(printf '%s\n' "$release_json" | parse_release_metadata)"; then - echo "Could not parse GitHub release metadata for Codex $requested_release." >&2 - exit 1 - fi + parse_downloaded_release_metadata "$requested_release" "GitHub" if [ "$normalized_version" = "latest" ]; then - release_tag="$(printf '%s\n' "$release_metadata" | awk -F '\t' '$1 == "tag_name" { print $2; exit }')" - case "$release_tag" in - rust-v*) resolved_version="${release_tag#rust-v}" ;; - *) resolved_version="" ;; - esac - if [ -z "$resolved_version" ]; then - echo "Failed to resolve the latest Codex release version." >&2 - exit 1 - fi - validate_version "$resolved_version" + resolve_metadata_version + resolved_version="$metadata_version" fi + + release_source="github" +} + +resolve_release_from_releases() { + normalized_version="$1" + + if [ "$normalized_version" = "latest" ]; then + requested_release="latest" + metadata_url="$RELEASES_BASE_URL/channels/latest" + else + requested_release="$normalized_version" + metadata_url="$RELEASES_BASE_URL/releases/$normalized_version/release.json" + fi + + if ! release_json="$(download_text "$metadata_url")"; then + return 1 + fi + + parse_downloaded_release_metadata "$requested_release" "releases.openai.com" + resolve_metadata_version + if [ "$normalized_version" != "latest" ] && [ "$metadata_version" != "$normalized_version" ]; then + echo "Release metadata version did not match requested Codex version $normalized_version." >&2 + exit 1 + fi + resolved_version="$metadata_version" + release_source="releases.openai.com" +} + +resolve_release() { + normalized_version="$(normalize_version "$RELEASE")" + validate_version "$normalized_version" + + 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() { @@ -321,6 +401,39 @@ 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_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" @@ -841,9 +954,13 @@ release_dir_is_complete() { esac case "$layout:$expected_target" in - package:*linux* | legacy-platform-npm:*linux*) [ -x "$release_dir/codex-resources/bwrap" ] ;; - *) true ;; + package:*linux* | legacy-platform-npm:*linux*) + [ -x "$release_dir/codex-resources/bwrap" ] || return 1 + ;; esac + + installed_version="$(version_from_binary "$release_dir/bin/codex" || version_from_binary "$release_dir/codex" || true)" + [ "$installed_version" = "$expected_version" ] } update_current_link() { @@ -949,21 +1066,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)" @@ -1003,13 +1106,13 @@ 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 "$checksum_url" "$checksum_path" + download_file_with_fallback "$checksum_url" "$checksum_fallback_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")" fi - download_file "$download_url" "$archive_path" + download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" verify_archive_digest "$archive_path" "$expected_digest" step "Installing standalone package to $release_dir" @@ -1019,6 +1122,10 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" install_legacy_platform_npm_release "$release_dir" "$archive_path" "$vendor_target" fi fi +if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" "$install_layout"; then + echo "Installed Codex command did not report expected version $resolved_version." >&2 + exit 1 +fi update_current_link "$release_dir" update_visible_command "$release_dir" add_to_path diff --git a/scripts/install/test_install_sh.py b/scripts/install/test_install_sh.py index fb38d3b582..f7c41ee45a 100644 --- a/scripts/install/test_install_sh.py +++ b/scripts/install/test_install_sh.py @@ -13,6 +13,7 @@ import unittest INSTALL_SCRIPT = Path(__file__).with_name("install.sh") VERSION = "0.142.5" +MISMATCH_VERSION = "0.145.0" class InstallShTest(unittest.TestCase): @@ -130,6 +131,143 @@ class InstallShTest(unittest.TestCase): ) self.assertTrue(os.access(host_path, os.X_OK)) + def test_releases_latest_installs_verified_package(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_releases=True, + ) + + 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}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + + def test_releases_asset_download_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) + + result, requests = run_installer_in( + root, + "latest", + metadata_json=metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_releases=True, + releases_mode="asset_fallback", + ) + + 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("retrying from GitHub Releases", result.stderr) + + def test_releases_exact_rejects_wrong_binary_version(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release( + root, + metadata_version=MISMATCH_VERSION, + ) + + result, requests = run_installer_in( + root, + MISMATCH_VERSION, + metadata_json=metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + use_releases=True, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + requests, + [ + f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/release.json", + f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/codex-package_SHA256SUMS", + f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/codex-package-aarch64-apple-darwin.tar.gz", + ], + ) + self.assertIn( + f"did not report expected version {MISMATCH_VERSION}", + result.stderr, + ) + self.assertNotIn("installed successfully", result.stdout) + + def test_releases_exact_legacy_fallback_reuses_offline_install(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, metadata_json = create_legacy_release(root) + + first_result, first_requests = run_installer_in( + root, + VERSION, + metadata_json=metadata_json, + legacy_archive_path=archive_path, + force_macos=True, + use_releases=True, + releases_mode="channel_failure", + ) + + self.assertEqual(first_result.returncode, 0, first_result.stderr) + self.assertEqual( + first_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-npm-darwin-arm64-{VERSION}.tgz", + ], + ) + + (root / "requests.log").unlink() + second_result, second_requests = run_installer_in( + root, + VERSION, + metadata_json=metadata_json, + force_macos=True, + use_releases=True, + releases_mode="channel_failure", + ) + + self.assertEqual(second_result.returncode, 0, second_result.stderr) + self.assertEqual( + second_requests, + [ + f"https://releases.openai.com/codex/releases/{VERSION}/release.json", + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}", + ], + ) + self.assertNotIn("Downloading Codex CLI", second_result.stdout) + def run_installer( release: str, @@ -154,10 +292,13 @@ def run_installer_in( metadata_json: str | None = None, archive_path: Path | None = None, checksum_path: Path | None = None, + legacy_archive_path: Path | None = None, force_macos: bool = False, + use_releases: bool = False, + releases_mode: str = "", ) -> tuple[subprocess.CompletedProcess[str], list[str]]: bin_dir = root / "bin" - bin_dir.mkdir() + bin_dir.mkdir(exist_ok=True) request_log = root / "requests.log" fake_curl = bin_dir / "curl" fake_curl.write_text( @@ -186,20 +327,53 @@ def run_installer_in( fi printf '%s\n' "$CODEX_TEST_METADATA_JSON" ;; - */codex-package_SHA256SUMS) + https://releases.openai.com/codex/channels/latest|https://releases.openai.com/codex/releases/*/release.json) + if [ "$CODEX_TEST_RELEASES_MODE" = "channel_failure" ]; then + exit 22 + fi + printf '%s\n' "$CODEX_TEST_RELEASES_METADATA_JSON" + ;; + https://releases.openai.com/codex/releases/*/codex-package_SHA256SUMS) + if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then + exit 22 + fi if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then cp "$CODEX_TEST_CHECKSUM_PATH" "$output" else exit 22 fi ;; - */codex-package-*.tar.gz) + https://releases.openai.com/codex/releases/*/codex-package-*.tar.gz) + if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then + exit 22 + fi if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then cp "$CODEX_TEST_ARCHIVE_PATH" "$output" else exit 22 fi ;; + https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS) + if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then + cp "$CODEX_TEST_CHECKSUM_PATH" "$output" + else + exit 22 + fi + ;; + https://github.com/openai/codex/releases/download/*/codex-package-*.tar.gz) + if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then + cp "$CODEX_TEST_ARCHIVE_PATH" "$output" + else + exit 22 + fi + ;; + https://github.com/openai/codex/releases/download/*/codex-npm-*.tgz) + if [ -n "$CODEX_TEST_LEGACY_ARCHIVE_PATH" ]; then + cp "$CODEX_TEST_LEGACY_ARCHIVE_PATH" "$output" + else + exit 22 + fi + ;; *) exit 22 ;; @@ -222,7 +396,7 @@ def run_installer_in( fake_uname.chmod(0o755) home = root / "home" - home.mkdir() + home.mkdir(exist_ok=True) env = os.environ.copy() env.update( { @@ -230,12 +404,20 @@ 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_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() + ), + "CODEX_TEST_RELEASES_MODE": releases_mode, "CODEX_TEST_REQUEST_LOG": str(request_log), "HOME": str(home), "PATH": f"{bin_dir}:/usr/bin:/bin", @@ -257,7 +439,11 @@ def run_installer_in( return result, requests -def create_package_release(root: Path) -> tuple[Path, Path, str]: +def create_package_release( + root: Path, + *, + metadata_version: str = VERSION, +) -> tuple[Path, Path, str]: package_dir = root / "package" (package_dir / "bin").mkdir(parents=True) (package_dir / "codex-path").mkdir() @@ -291,13 +477,40 @@ def create_package_release(root: Path) -> tuple[Path, Path, str]: "digest": f"sha256:{checksum_digest}", }, ], - "tag_name": f"rust-v{VERSION}", + "tag_name": f"rust-v{metadata_version}", }, indent=2, ) return archive_path, checksum_path, metadata_json +def create_legacy_release(root: Path) -> tuple[Path, str]: + package_dir = root / "legacy-package" + vendor_dir = package_dir / "package" / "vendor" / "aarch64-apple-darwin" + (vendor_dir / "codex").mkdir(parents=True) + (vendor_dir / "path").mkdir() + write_executable( + vendor_dir / "codex" / "codex", + f"#!/bin/sh\nprintf 'codex-cli {VERSION}\\n'\n", + ) + write_executable(vendor_dir / "path" / "rg", "#!/bin/sh\nexit 0\n") + + asset = f"codex-npm-darwin-arm64-{VERSION}.tgz" + archive_path = root / asset + with tarfile.open(archive_path, "w:gz") as archive: + archive.add(package_dir / "package", arcname="package") + + archive_digest = hashlib.sha256(archive_path.read_bytes()).hexdigest() + metadata_json = json.dumps( + { + "assets": [{"name": asset, "digest": f"sha256:{archive_digest}"}], + "tag_name": f"rust-v{VERSION}", + }, + indent=2, + ) + return archive_path, metadata_json + + def write_executable(path: Path, contents: str) -> None: path.write_text(contents, encoding="utf-8") path.chmod(0o755)