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
This commit is contained in:
zsol-openai
2026-07-21 10:34:10 +00:00
committed by copyberry
parent d937bfac84
commit 765675a122
3 changed files with 529 additions and 73 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)