mirror of
https://github.com/openai/codex.git
synced 2026-05-28 06:55:01 +00:00
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 | | --- | --- | |  |  | 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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 <<EOF
|
||||
Usage: install.sh [--release VERSION]
|
||||
|
||||
Environment:
|
||||
CODEX_RELEASE Version to install; overridden by --release.
|
||||
CODEX_NON_INTERACTIVE Set to 1, true, or yes to skip prompts.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
@@ -259,6 +277,7 @@ require_command() {
|
||||
|
||||
resolve_version() {
|
||||
normalized_version="$(normalize_version "$RELEASE")"
|
||||
validate_version "$normalized_version"
|
||||
|
||||
if [ "$normalized_version" != "latest" ]; then
|
||||
printf '%s\n' "$normalized_version"
|
||||
@@ -273,6 +292,7 @@ resolve_version() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_version "$resolved"
|
||||
printf '%s\n' "$resolved"
|
||||
}
|
||||
|
||||
@@ -304,7 +324,9 @@ add_to_path() {
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*)
|
||||
return
|
||||
if [ -z "$conflict_manager" ]; then
|
||||
return
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -544,6 +566,12 @@ classify_existing_codex() {
|
||||
prompt_yes_no() {
|
||||
prompt="$1"
|
||||
|
||||
case "$NON_INTERACTIVE" in
|
||||
1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss])
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ( : </dev/tty ) 2>/dev/null; then
|
||||
printf '%s [y/N] ' "$prompt" >/dev/tty
|
||||
if ! IFS= read -r answer </dev/tty; then
|
||||
|
||||
Reference in New Issue
Block a user