mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user