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
This commit is contained in:
zsol-openai
2026-07-23 09:16:41 +00:00
committed by copyberry
parent 4462b9deef
commit 39a2438d16
4 changed files with 416 additions and 62 deletions

View File

@@ -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" 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: Codex CLI can also be installed via the following package managers:
```shell ```shell

View File

@@ -12,13 +12,15 @@ if ([string]::IsNullOrWhiteSpace($Release)) {
} }
$NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" $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)) { $PreferReleasesOpenAICom = if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM)) {
$DefaultPreferReleasesOpenAICom $DefaultPreferReleasesOpenAICom
} else { } else {
$env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM -match "^(?i:1|true|yes)$" $env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM -match "^(?i:1|true|yes)$"
} }
$ReleasesBaseUri = "https://releases.openai.com/codex" $ReleasesBaseUri = "https://releases.openai.com/codex"
$ReleasesMetadataTimeoutSec = 30
$ReleasesAssetTimeoutSec = 300
function Write-Step { function Write-Step {
param( param(
@@ -111,17 +113,45 @@ function Find-ReleaseAssetMetadata {
function Invoke-WebRequestWithFallback { function Invoke-WebRequestWithFallback {
param( param(
[object]$Metadata, [object]$Metadata,
[string]$OutFile [string]$OutFile,
[string]$ExpectedDigest,
[string]$AssetName,
[string]$ReleaseVersion,
[string]$RequiredManifestAsset
) )
try { 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 { } catch {
if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) { if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) {
throw 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 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" "$ReleasesBaseUri/releases/$NormalizedVersion/release.json"
} }
try { 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 { } catch {
return $null return $null
} }
try { return $resolvedRelease
$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 { function Resolve-Release {
@@ -932,14 +959,12 @@ try {
Write-Step "Downloading Codex CLI" Write-Step "Downloading Codex CLI"
if ($installLayout -eq "Package") { if ($installLayout -eq "Package") {
Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath -ExpectedDigest $checksumMetadata.Sha256 -AssetName $checksumAsset -ReleaseVersion $resolvedVersion -RequiredManifestAsset $packageAsset
Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256
$expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset
} else { } else {
$expectedPackageDigest = $packageMetadata.Sha256 $expectedPackageDigest = $packageMetadata.Sha256
} }
Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath -ExpectedDigest $expectedPackageDigest -AssetName $packageAsset -ReleaseVersion $resolvedVersion
Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest
New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null
if (Test-Path -LiteralPath $stagingDir) { if (Test-Path -LiteralPath $stagingDir) {

View File

@@ -4,9 +4,12 @@ set -eu
RELEASE="${CODEX_RELEASE:-latest}" RELEASE="${CODEX_RELEASE:-latest}"
NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" 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}" PREFER_RELEASES_OPENAI_COM="${CODEX_INSTALLER_USE_RELEASES_OPENAI_COM:-$DEFAULT_PREFER_RELEASES_OPENAI_COM}"
RELEASES_BASE_URL="https://releases.openai.com/codex" RELEASES_BASE_URL="https://releases.openai.com/codex"
RELEASES_CONNECT_TIMEOUT=10
RELEASES_METADATA_TIMEOUT=30
RELEASES_ASSET_TIMEOUT=300
release_source="github" release_source="github"
BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" 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 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 echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]." >&2
exit 1 return 1
fi fi
} }
@@ -83,6 +86,8 @@ Usage: install.sh [--release VERSION]
Environment: Environment:
CODEX_RELEASE Version to install; overridden by --release. CODEX_RELEASE Version to install; overridden by --release.
CODEX_NON_INTERACTIVE Set to 1, true, or yes to skip prompts. 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 EOF
exit 0 exit 0
;; ;;
@@ -100,12 +105,26 @@ download_file() {
output="$2" output="$2"
if command -v curl >/dev/null 2>&1; then 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 return
fi fi
if command -v wget >/dev/null 2>&1; then 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 return
fi fi
@@ -117,12 +136,26 @@ download_text() {
url="$1" url="$1"
if command -v curl >/dev/null 2>&1; then 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 return
fi fi
if command -v wget >/dev/null 2>&1; then 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 return
fi fi
@@ -134,8 +167,13 @@ download_file_with_fallback() {
primary_url="$1" primary_url="$1"
fallback_url="$2" fallback_url="$2"
output="$3" 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 return
fi fi
@@ -143,8 +181,19 @@ download_file_with_fallback() {
return 1 return 1
fi 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" 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() { parse_release_metadata() {
@@ -279,7 +328,7 @@ parse_downloaded_release_metadata() {
source_name="$2" source_name="$2"
if ! release_metadata="$(printf '%s\n' "$release_json" | parse_release_metadata)"; then 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 echo "Could not parse $source_name release metadata for Codex $requested_release." >&2
exit 1 return 1
fi fi
} }
@@ -291,7 +340,7 @@ resolve_metadata_version() {
esac esac
if [ -z "$metadata_version" ]; then if [ -z "$metadata_version" ]; then
echo "Failed to resolve the latest Codex release version." >&2 echo "Failed to resolve the latest Codex release version." >&2
exit 1 return 1
fi fi
validate_version "$metadata_version" validate_version "$metadata_version"
} }
@@ -337,11 +386,15 @@ resolve_release_from_releases() {
return 1 return 1
fi fi
parse_downloaded_release_metadata "$requested_release" "releases.openai.com" if ! parse_downloaded_release_metadata "$requested_release" "releases.openai.com"; then
resolve_metadata_version return 1
fi
if ! resolve_metadata_version; then
return 1
fi
if [ "$normalized_version" != "latest" ] && [ "$metadata_version" != "$normalized_version" ]; then if [ "$normalized_version" != "latest" ] && [ "$metadata_version" != "$normalized_version" ]; then
echo "Release metadata version did not match requested Codex version $normalized_version." >&2 echo "Release metadata version did not match requested Codex version $normalized_version." >&2
exit 1 return 1
fi fi
resolved_version="$metadata_version" resolved_version="$metadata_version"
release_source="releases.openai.com" release_source="releases.openai.com"
@@ -353,7 +406,8 @@ resolve_release() {
case "$PREFER_RELEASES_OPENAI_COM" in case "$PREFER_RELEASES_OPENAI_COM" in
1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss]) 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 return
fi fi
warn "releases.openai.com is unavailable; falling back to GitHub Releases." warn "releases.openai.com is unavailable; falling back to GitHub Releases."
@@ -361,6 +415,7 @@ resolve_release() {
esac esac
resolve_release_from_github "$normalized_version" resolve_release_from_github "$normalized_version"
select_release_assets
} }
release_asset_digest_or_empty() { release_asset_digest_or_empty() {
@@ -375,7 +430,11 @@ release_asset_digest_or_empty() {
case "$digest" in case "$digest" in
sha256:????????????????????????????????????????????????????????????????) 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 return 1
@@ -416,7 +475,7 @@ select_release_assets() {
asset="codex-npm-$npm_tag-$resolved_version.tgz" asset="codex-npm-$npm_tag-$resolved_version.tgz"
else else
echo "Could not find Codex package or platform npm release assets for Codex $resolved_version." >&2 echo "Could not find Codex package or platform npm release assets for Codex $resolved_version." >&2
exit 1 return 1
fi fi
if [ "$release_source" = "releases.openai.com" ]; then if [ "$release_source" = "releases.openai.com" ]; then
@@ -453,7 +512,7 @@ package_archive_digest() {
if [ -z "$digest" ]; then if [ -z "$digest" ]; then
echo "Could not find SHA-256 digest for $asset in codex-package_SHA256SUMS." >&2 echo "Could not find SHA-256 digest for $asset in codex-package_SHA256SUMS." >&2
exit 1 return 1
fi fi
printf '%s\n' "$digest" printf '%s\n' "$digest"
@@ -490,7 +549,7 @@ verify_archive_digest() {
echo "Downloaded Codex archive checksum did not match expected digest." >&2 echo "Downloaded Codex archive checksum did not match expected digest." >&2
echo "expected: $expected_digest" >&2 echo "expected: $expected_digest" >&2
echo "actual: $actual_digest" >&2 echo "actual: $actual_digest" >&2
exit 1 return 1
fi fi
} }
@@ -1066,7 +1125,6 @@ else
fi fi
resolve_release resolve_release
select_release_assets
release_name="$resolved_version-$vendor_target" release_name="$resolved_version-$vendor_target"
release_dir="$RELEASES_DIR/$release_name" release_dir="$RELEASES_DIR/$release_name"
current_version="$(current_installed_version)" current_version="$(current_installed_version)"
@@ -1106,14 +1164,12 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target"
step "Downloading Codex CLI" step "Downloading Codex CLI"
if [ "$install_layout" = "package" ]; then if [ "$install_layout" = "package" ]; then
checksum_digest="$(release_asset_digest "$checksum_asset")" checksum_digest="$(release_asset_digest "$checksum_asset")"
download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" "$checksum_digest" "$checksum_asset" "$asset"
verify_archive_digest "$checksum_path" "$checksum_digest"
expected_digest="$(package_archive_digest "$asset" "$checksum_path")" expected_digest="$(package_archive_digest "$asset" "$checksum_path")"
else else
expected_digest="$(release_asset_digest "$asset")" expected_digest="$(release_asset_digest "$asset")"
fi fi
download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" "$expected_digest" "$asset"
verify_archive_digest "$archive_path" "$expected_digest"
step "Installing standalone package to $release_dir" step "Installing standalone package to $release_dir"
if [ "$install_layout" = "package" ]; then if [ "$install_layout" = "package" ]; then

View File

@@ -34,8 +34,8 @@ class InstallShTest(unittest.TestCase):
) )
self.assertNotIn("Could not find Codex package", result.stderr) self.assertNotIn("Could not find Codex package", result.stderr)
def test_exact_release_fetches_metadata_once(self) -> None: def test_exact_release_opt_out_uses_github_metadata_once(self) -> None:
result, requests = run_installer(VERSION) result, requests = run_installer(VERSION, use_mirror=False)
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)
self.assertEqual( self.assertEqual(
@@ -131,7 +131,7 @@ class InstallShTest(unittest.TestCase):
) )
self.assertTrue(os.access(host_path, os.X_OK)) 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: with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) root = Path(temp_dir)
archive_path, checksum_path, metadata_json = create_package_release(root) archive_path, checksum_path, metadata_json = create_package_release(root)
@@ -143,7 +143,7 @@ class InstallShTest(unittest.TestCase):
archive_path=archive_path, archive_path=archive_path,
checksum_path=checksum_path, checksum_path=checksum_path,
force_macos=True, force_macos=True,
use_releases=True, use_mirror=None,
) )
self.assertEqual(result.returncode, 0, result.stderr) 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": "<html>proxy error</html>",
"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: def test_releases_asset_download_falls_back_to_github(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) root = Path(temp_dir)
@@ -168,7 +263,7 @@ class InstallShTest(unittest.TestCase):
archive_path=archive_path, archive_path=archive_path,
checksum_path=checksum_path, checksum_path=checksum_path,
force_macos=True, force_macos=True,
use_releases=True, use_mirror=None,
releases_mode="asset_fallback", releases_mode="asset_fallback",
) )
@@ -187,6 +282,147 @@ class InstallShTest(unittest.TestCase):
) )
self.assertIn("retrying from GitHub Releases", result.stderr) 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: def test_releases_exact_rejects_wrong_binary_version(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) root = Path(temp_dir)
@@ -202,7 +438,7 @@ class InstallShTest(unittest.TestCase):
archive_path=archive_path, archive_path=archive_path,
checksum_path=checksum_path, checksum_path=checksum_path,
force_macos=True, force_macos=True,
use_releases=True, use_mirror=True,
) )
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)
@@ -231,7 +467,7 @@ class InstallShTest(unittest.TestCase):
metadata_json=metadata_json, metadata_json=metadata_json,
legacy_archive_path=archive_path, legacy_archive_path=archive_path,
force_macos=True, force_macos=True,
use_releases=True, use_mirror=True,
releases_mode="channel_failure", releases_mode="channel_failure",
) )
@@ -253,7 +489,7 @@ class InstallShTest(unittest.TestCase):
VERSION, VERSION,
metadata_json=metadata_json, metadata_json=metadata_json,
force_macos=True, force_macos=True,
use_releases=True, use_mirror=True,
releases_mode="channel_failure", releases_mode="channel_failure",
) )
@@ -274,6 +510,7 @@ def run_installer(
*, *,
metadata_failure: bool = False, metadata_failure: bool = False,
metadata_json: str | None = None, metadata_json: str | None = None,
use_mirror: bool | None = False,
) -> tuple[subprocess.CompletedProcess[str], list[str]]: ) -> tuple[subprocess.CompletedProcess[str], list[str]]:
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
return run_installer_in( return run_installer_in(
@@ -281,6 +518,7 @@ def run_installer(
release, release,
metadata_failure=metadata_failure, metadata_failure=metadata_failure,
metadata_json=metadata_json, metadata_json=metadata_json,
use_mirror=use_mirror,
) )
@@ -290,11 +528,13 @@ def run_installer_in(
*, *,
metadata_failure: bool = False, metadata_failure: bool = False,
metadata_json: str | None = None, metadata_json: str | None = None,
releases_metadata_json: str | None = None,
archive_path: Path | None = None, archive_path: Path | None = None,
checksum_path: Path | None = None, checksum_path: Path | None = None,
releases_checksum_path: Path | None = None,
legacy_archive_path: Path | None = None, legacy_archive_path: Path | None = None,
force_macos: bool = False, force_macos: bool = False,
use_releases: bool = False, use_mirror: bool | None = False,
releases_mode: str = "", releases_mode: str = "",
) -> tuple[subprocess.CompletedProcess[str], list[str]]: ) -> tuple[subprocess.CompletedProcess[str], list[str]]:
bin_dir = root / "bin" bin_dir = root / "bin"
@@ -337,8 +577,13 @@ def run_installer_in(
if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then
exit 22 exit 22
fi fi
if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ] ||
cp "$CODEX_TEST_CHECKSUM_PATH" "$output" [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then
printf '<html>proxy error</html>\n' >"$output"
exit 0
fi
if [ -n "$CODEX_TEST_RELEASES_CHECKSUM_PATH" ]; then
cp "$CODEX_TEST_RELEASES_CHECKSUM_PATH" "$output"
else else
exit 22 exit 22
fi fi
@@ -347,6 +592,10 @@ def run_installer_in(
if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then
exit 22 exit 22
fi fi
if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ]; then
printf '<html>proxy error</html>\n' >"$output"
exit 0
fi
if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then
cp "$CODEX_TEST_ARCHIVE_PATH" "$output" cp "$CODEX_TEST_ARCHIVE_PATH" "$output"
else else
@@ -354,6 +603,10 @@ def run_installer_in(
fi fi
;; ;;
https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS) https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS)
if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then
printf '<html>proxy error</html>\n' >"$output"
exit 0
fi
if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then
cp "$CODEX_TEST_CHECKSUM_PATH" "$output" cp "$CODEX_TEST_CHECKSUM_PATH" "$output"
else else
@@ -404,18 +657,22 @@ def run_installer_in(
"CODEX_INSTALL_DIR": str(root / "install-bin"), "CODEX_INSTALL_DIR": str(root / "install-bin"),
"CODEX_NON_INTERACTIVE": "1", "CODEX_NON_INTERACTIVE": "1",
"CODEX_RELEASE": release, "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_ARCHIVE_PATH": str(archive_path or ""),
"CODEX_TEST_CHECKSUM_PATH": str(checksum_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_LEGACY_ARCHIVE_PATH": str(legacy_archive_path or ""),
"CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0",
"CODEX_TEST_METADATA_JSON": ( "CODEX_TEST_METADATA_JSON": (
metadata_json if metadata_json is not None else release_metadata() metadata_json if metadata_json is not None else release_metadata()
), ),
"CODEX_TEST_RELEASES_METADATA_JSON": ( "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_RELEASES_MODE": releases_mode,
"CODEX_TEST_REQUEST_LOG": str(request_log), "CODEX_TEST_REQUEST_LOG": str(request_log),
@@ -424,6 +681,12 @@ def run_installer_in(
"SHELL": "/bin/sh", "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( result = subprocess.run(
["/bin/sh", str(INSTALL_SCRIPT)], ["/bin/sh", str(INSTALL_SCRIPT)],
capture_output=True, capture_output=True,