From 8ed38fe38eb99dd51a0eef33366a45a392733b82 Mon Sep 17 00:00:00 2001 From: efrazer-oai Date: Tue, 26 May 2026 22:09:54 -0700 Subject: [PATCH] fix: add noninteractive install script mode (#21567) # Summary The Codex standalone installers can pause after installation to ask about an older managed install or launching Codex. That makes unattended bootstrap and update flows hard to complete reliably. This PR adds noninteractive installer control on macOS/Linux and Windows through `CODEX_NON_INTERACTIVE=1`. Noninteractive operation is environment-only, which gives automated callers one stable way to suppress prompts. When a noninteractive install leaves an older npm, bun, or brew-managed Codex installed, the standalone bin is configured ahead of that command on `PATH` so the newly installed Codex is the one future launches select. It also supports `CODEX_RELEASE` for callers that select a release through environment variables while retaining the existing explicit release inputs. Release selection accepts `latest`, stable `x.y.z` versions, and Codex prereleases written as `rust-v0.134.0-alpha.3`, `v0.134.0-alpha.3`, or `0.134.0-alpha.3`; it validates that shape before constructing release requests. # Stack 1. [#21567](https://github.com/openai/codex/pull/21567) - Adds release and noninteractive environment controls to the installers. (current) 2. [#24637](https://github.com/openai/codex/pull/24637) - Runs standalone updater installs with `CODEX_NON_INTERACTIVE=1`. 3. [#24639](https://github.com/openai/codex/pull/24639) - Removes explicit release argument inputs in favor of `CODEX_RELEASE`. # Evidence | Before | After | | --- | --- | | ![Interactive install prompts](https://github.com/user-attachments/assets/feecb45a-7087-4681-8775-ba57b07e97fa) | ![Noninteractive install completes without prompts](https://github.com/user-attachments/assets/53dcc791-383a-46e2-9a95-3b37b80ae053) | Environment-controlled macOS install with an existing npm-managed Codex on `PATH`: https://github.com/user-attachments/assets/442e0b5b-4a32-4bf5-996b-68784777380d # Design decisions Windows installs using the older standalone bin layout still require an interactive migration confirmation. Noninteractive mode does not auto-migrate that existing directory because replacing it is a destructive transition for an early, limited-use layout; unattended installs on that layout fail with an instruction to rerun interactively. # Testing Tests: installer syntax validation, release-selector acceptance and rejection coverage including PowerShell `Latest` compatibility, macOS live-terminal installer smoke testing with environment-controlled stable and prerelease installation and competing PATH precedence, shell rejection of the omitted noninteractive flag, and Windows ARM64 PowerShell smoke testing with environment-only noninteractive behavior, retained release input, and competing PATH precedence through Parallels. --- scripts/install/install.ps1 | 59 ++++++++++++++++++++++++++++++++++--- scripts/install/install.sh | 32 ++++++++++++++++++-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 8ebc4fbb82..6973d482e2 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -1,11 +1,18 @@ +[CmdletBinding()] param( - [string]$Release = "latest" + [string]$Release = $env:CODEX_RELEASE ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" +if ([string]::IsNullOrWhiteSpace($Release)) { + $Release = "latest" +} + +$NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" + function Write-Step { param( [string]$Message @@ -27,6 +34,10 @@ function Prompt-YesNo { [string]$Prompt ) + if ($NonInteractive) { + return $false + } + if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) { return $false } @@ -55,6 +66,16 @@ function Normalize-Version { return $RawVersion } +function Assert-ValidReleaseVersion { + param( + [string]$Version + ) + + if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+)?)?$") { + throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]." + } +} + function Find-ReleaseAssetMetadata { param( [string]$AssetName, @@ -141,6 +162,22 @@ function Path-Contains { return $false } +function Prepend-PathEntry { + param( + [string]$PathValue, + [string]$Entry + ) + + $needle = $Entry.TrimEnd("\") + $segments = @($Entry) + if (-not [string]::IsNullOrWhiteSpace($PathValue)) { + $segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) | + Where-Object { $_.TrimEnd("\") -ine $needle } + } + + return ($segments -join ";") +} + function Invoke-WithInstallLock { param( [string]$LockPath, @@ -181,6 +218,7 @@ function Remove-StaleInstallArtifacts { function Resolve-Version { $normalizedVersion = Normalize-Version -RawVersion $Release + Assert-ValidReleaseVersion -Version $normalizedVersion if ($normalizedVersion -ne "latest") { return $normalizedVersion } @@ -191,7 +229,9 @@ function Resolve-Version { exit 1 } - return (Normalize-Version -RawVersion $release.tag_name) + $resolvedVersion = Normalize-Version -RawVersion $release.tag_name + Assert-ValidReleaseVersion -Version $resolvedVersion + return $resolvedVersion } function Get-VersionFromBinary { @@ -839,7 +879,16 @@ try { Maybe-HandleConflictingInstall -Conflict $conflictingInstall $userPath = [Environment]::GetEnvironmentVariable("Path", "User") -if (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { +$prioritizeVisibleBin = $null -ne $conflictingInstall +if ($prioritizeVisibleBin) { + $newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir + if ($newUserPath -cne $userPath) { + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + Write-Step "PATH updated for future PowerShell sessions." + } else { + Write-Step "$visibleBinDir is already first on PATH." + } +} elseif (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { if ([string]::IsNullOrWhiteSpace($userPath)) { $newUserPath = $visibleBinDir } else { @@ -854,7 +903,9 @@ if (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { Write-Step "PATH is already configured for future PowerShell sessions." } -if (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) { +if ($prioritizeVisibleBin) { + $env:Path = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir +} elseif (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) { if ([string]::IsNullOrWhiteSpace($env:Path)) { $env:Path = $visibleBinDir } else { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index cc4fc91345..23980b0c5e 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -2,7 +2,8 @@ set -eu -RELEASE="latest" +RELEASE="${CODEX_RELEASE:-latest}" +NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" @@ -46,6 +47,19 @@ normalize_version() { esac } +validate_version() { + version="$1" + + if [ "$version" = "latest" ]; then + return + fi + + if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$'; then + echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]." >&2 + exit 1 + fi +} + parse_args() { while [ "$#" -gt 0 ]; do case "$1" in @@ -60,6 +74,10 @@ parse_args() { --help | -h) cat </dev/null; then printf '%s [y/N] ' "$prompt" >/dev/tty if ! IFS= read -r answer