4 Commits
Author SHA1 Message Date
fourbroad e3098f2225 ci: sync from octopus@0.15.0 2026-07-05 13:45:03 +08:00
fourbroadandGitea Server e79718a5fb Merge pull request 'fix(install): do not kill PowerShell host under 'irm | iex'' (#1) from workflow/bugfix/install-exit-kills-iex-host into main
Reviewed-on: http://octopus.51zxtx.com:3000/fourbroad/octopus-release/pulls/1
2026-07-02 09:54:18 +08:00
octopus-agent fbac1003f0 fix(install): do not kill PowerShell host under 'irm | iex'
The Windows installer used 'exit' inside Install-Octopus. When run via
'irm <url> | iex' (the documented one-liner), there is no script scope, so
'exit' terminates the entire PowerShell host process and closes the terminal
window instead of just ending the install. Most users hit this via the
'already installed' branch (exit 0).

Replace 'exit' with:
  - 'return' for the success (already-installed) path
  - 'throw' for error paths (still non-zero exit when run as .\install.ps1
    in CI; prints the error and returns to the prompt under iex)

Add tests/test.ps1 as a regression guard (source-verification + a behavioral
test proving iex no longer terminates the host).
2026-07-02 09:10:03 +08:00
fourbroad 075445250f ci: sync from octopus@0.9.0 2026-06-23 08:06:46 +08:00
4 changed files with 766 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Octopus
The AI coding agent built for the terminal.
## Install
**Unix (Linux/macOS):**
```bash
curl -fsSL http://octopus.51zxtx.com:3000/fourbroad/octopus-release/raw/branch/main/install | bash
```
**Windows (PowerShell):**
```powershell
irm http://octopus.51zxtx.com:3000/fourbroad/octopus-release/raw/branch/main/install.ps1 | iex
```
## Documentation
Visit the [documentation](http://octopus.51zxtx.com:3000/fourbroad/octopus/wiki) for usage guides and API reference.
## Development
The source code and issue tracker are available at the [private development repository](http://octopus.51zxtx.com:3000/fourbroad/octopus).
## License
MIT License — see the source repository for details.
Executable
+461
View File
@@ -0,0 +1,461 @@
#!/usr/bin/env bash
# Public release repository:
# http://octopus.51zxtx.com:3000/fourbroad/octopus-release
set -euo pipefail
APP=octopus
MUTED='\033[0;2m'
RED='\033[0;31m'
ORANGE='\033[38;5;214m'
NC='\033[0m' # No Color
usage() {
cat <<EOF
Octopus Installer
Usage: install.sh [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 1.0.180)
-b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples:
curl -fsSL https://octopus.51zxtx.com/install | bash
curl -fsSL https://octopus.51zxtx.com/install | bash -s -- --version 1.0.180
./install --binary /path/to/octopus
EOF
}
requested_version=${VERSION:-}
no_modify_path=false
binary_path=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--version)
if [[ -n "${2:-}" ]]; then
requested_version="$2"
shift 2
else
echo -e "${RED}Error: --version requires a version argument${NC}"
exit 1
fi
;;
-b|--binary)
if [[ -n "${2:-}" ]]; then
binary_path="$2"
shift 2
else
echo -e "${RED}Error: --binary requires a path argument${NC}"
exit 1
fi
;;
--no-modify-path)
no_modify_path=true
shift
;;
*)
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
shift
;;
esac
done
INSTALL_DIR=$HOME/.octopus/bin
mkdir -p "$INSTALL_DIR"
# If --binary is provided, skip all download/detection logic
if [ -n "$binary_path" ]; then
if [ ! -f "$binary_path" ]; then
echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
exit 1
fi
specific_version="local"
else
raw_os=$(uname -s)
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
case "$raw_os" in
Darwin*) os="darwin" ;;
Linux*) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
esac
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
fi
if [[ "$arch" == "x86_64" ]]; then
arch="x64"
fi
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
if [ "$rosetta_flag" = "1" ]; then
arch="arm64"
fi
fi
combo="$os-$arch"
case "$combo" in
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
;;
*)
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
exit 1
;;
esac
archive_ext=".zip"
if [ "$os" = "linux" ]; then
archive_ext=".tar.gz"
fi
is_musl=false
if [ "$os" = "linux" ]; then
if [ -f /etc/alpine-release ]; then
is_musl=true
fi
if command -v ldd >/dev/null 2>&1; then
if ldd --version 2>&1 | grep -qi musl; then
is_musl=true
fi
fi
fi
needs_baseline=false
if [ "$arch" = "x64" ]; then
if [ "$os" = "linux" ]; then
if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then
needs_baseline=true
fi
fi
if [ "$os" = "darwin" ]; then
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
if [ "$avx2" != "1" ]; then
needs_baseline=true
fi
fi
if [ "$os" = "windows" ]; then
ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)"
out=""
if command -v powershell.exe >/dev/null 2>&1; then
out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
elif command -v pwsh >/dev/null 2>&1; then
out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
fi
out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
if [ "$out" != "true" ] && [ "$out" != "1" ]; then
needs_baseline=true
fi
fi
fi
target="$os-$arch"
if [ "$needs_baseline" = "true" ]; then
target="$target-baseline"
fi
if [ "$is_musl" = "true" ]; then
target="$target-musl"
fi
filename="$APP-$target$archive_ext"
if [ "$os" = "linux" ]; then
if ! command -v tar >/dev/null 2>&1; then
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
exit 1
fi
else
if ! command -v unzip >/dev/null 2>&1; then
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
exit 1
fi
fi
if [ -z "$requested_version" ]; then
specific_version=$(curl -s "${GITEA_API_URL:-http://octopus.51zxtx.com:3000/api/v1}/repos/fourbroad/octopus-release/releases?limit=1" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
exit 1
fi
url="${OCTOPUS_DOWNLOAD_BASE:-http://octopus.51zxtx.com:3000}/fourbroad/octopus-release/releases/download/v${specific_version}/$filename"
else
# Strip leading 'v' if present
requested_version="${requested_version#v}"
url="${OCTOPUS_DOWNLOAD_BASE:-http://octopus.51zxtx.com:3000}/fourbroad/octopus-release/releases/download/v${requested_version}/$filename"
specific_version=$requested_version
# Verify the release exists before downloading
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "${GITEA_URL:-http://octopus.51zxtx.com:3000}/fourbroad/octopus-release/releases/tag/v${requested_version}")
if [ "$http_status" = "404" ]; then
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
echo -e "${MUTED}Available releases: ${GITEA_URL:-http://octopus.51zxtx.com:3000}/fourbroad/octopus-release/releases${NC}"
exit 1
fi
fi
fi
print_message() {
local level=$1
local message=$2
local color=""
case $level in
info) color="${NC}" ;;
warning) color="${NC}" ;;
error) color="${RED}" ;;
esac
echo -e "${color}${message}${NC}"
}
check_version() {
if command -v octopus >/dev/null 2>&1; then
octopus_path=$(which octopus)
## Check the installed version
installed_version=$(octopus --version 2>/dev/null || echo "")
if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version."
else
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
exit 0
fi
fi
}
unbuffered_sed() {
if echo | sed -u -e "" >/dev/null 2>&1; then
sed -nu "$@"
elif echo | sed -l -e "" >/dev/null 2>&1; then
sed -nl "$@"
else
local pad="$(printf "\n%512s" "")"
sed -ne "s/$/\\${pad}/" "$@"
fi
}
print_progress() {
local bytes="$1"
local length="$2"
[ "$length" -gt 0 ] || return 0
local width=50
local percent=$(( bytes * 100 / length ))
[ "$percent" -gt 100 ] && percent=100
local on=$(( percent * width / 100 ))
local off=$(( width - on ))
local filled=$(printf "%*s" "$on" "")
filled=${filled// /■}
local empty=$(printf "%*s" "$off" "")
empty=${empty// /・}
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
}
download_with_progress() {
local url="$1"
local output="$2"
if [ -t 2 ]; then
exec 4>&2
else
exec 4>/dev/null
fi
local tmp_dir=${TMPDIR:-/tmp}
local basename="${tmp_dir}/octopus_install_$$"
local tracefile="${basename}.trace"
rm -f "$tracefile"
mkfifo "$tracefile"
# Hide cursor
printf "\033[?25l" >&4
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
(
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
) &
local curl_pid=$!
unbuffered_sed \
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
-e '/^0000: content-length:/p' \
-e '/^<= recv data/p' \
"$tracefile" | \
{
local length=0
local bytes=0
while IFS=" " read -r -a line; do
[ "${#line[@]}" -lt 2 ] && continue
local tag="${line[0]} ${line[1]}"
if [ "$tag" = "0000: content-length:" ]; then
length="${line[2]}"
length=$(echo "$length" | tr -d '\r')
bytes=0
elif [ "$tag" = "<= recv" ]; then
local size="${line[3]}"
bytes=$(( bytes + size ))
if [ "$length" -gt 0 ]; then
print_progress "$bytes" "$length"
fi
fi
done
}
wait $curl_pid
local ret=$?
echo "" >&4
return $ret
}
download_and_install() {
print_message info "\n${MUTED}Installing ${NC}octopus ${MUTED}version: ${NC}$specific_version"
local tmp_dir="${TMPDIR:-/tmp}/octopus_install_$$"
mkdir -p "$tmp_dir"
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
curl -# -L -o "$tmp_dir/$filename" "$url"
fi
if [ "$os" = "linux" ]; then
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
else
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
fi
mv "$tmp_dir/octopus" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/octopus"
rm -rf "$tmp_dir"
}
install_from_binary() {
print_message info "\n${MUTED}Installing ${NC}octopus ${MUTED}from: ${NC}$binary_path"
cp "$binary_path" "${INSTALL_DIR}/octopus"
chmod 755 "${INSTALL_DIR}/octopus"
}
if [ -n "$binary_path" ]; then
install_from_binary
else
check_version
download_and_install
fi
add_to_path() {
local config_file=$1
local command=$2
if grep -Fxq "$command" "$config_file"; then
print_message info "Command already exists in $config_file, skipping write."
elif [[ -w $config_file ]]; then
echo -e "\n# octopus" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "${MUTED}Successfully added ${NC}octopus ${MUTED}to \$PATH in ${NC}$config_file"
else
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " $command"
fi
}
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
current_shell=$(basename "$SHELL")
case $current_shell in
fish)
config_files="$HOME/.config/fish/config.fish"
;;
zsh)
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
;;
bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;;
ash)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;;
sh)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;;
*)
# Default case if none of the above matches
config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;;
esac
if [[ "$no_modify_path" != "true" ]]; then
config_file=""
for file in $config_files; do
if [[ -f $file ]]; then
config_file=$file
break
fi
done
if [[ -z $config_file ]]; then
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in
fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;;
zsh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
bash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
ash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
sh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;
esac
fi
fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> $GITHUB_PATH
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
echo -e ""
echo -e "\033[38;5;214m█▀▀█ █▀▀▀ ▀▀█▀▀ █▀▀█ █▀▀█ █ █ █▀▀▀${NC}"
echo -e "\033[38;5;172m█ █ █ █ █ █ █▀▀▀ █ █ ▀▀▀█${NC}"
echo -e "\033[38;5;130m▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ ▀ ▀▀▀▀ ▀▀▀▀${NC}"
echo -e ""
echo -e ""
echo -e "${MUTED}Octopus includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "octopus ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://octopus.51zxtx.com/docs"
echo -e ""
echo -e ""
+201
View File
@@ -0,0 +1,201 @@
#Requires -Version 5.1
# Public release repository: http://octopus.51zxtx.com:3000/fourbroad/octopus-release
<#
.SYNOPSIS
Octopus installer for Windows.
.DESCRIPTION
Downloads and installs the Octopus CLI from Gitea Releases.
.PARAMETER Version
Install a specific version (e.g. 1.0.180). Defaults to latest.
.PARAMETER NoModifyPath
Do not add the install directory to the user PATH.
.EXAMPLE
irm http://octopus.51zxtx.com:3000/fourbroad/octopus-release/raw/branch/dev/install.ps1 | iex
.EXAMPLE
irm http://octopus.51zxtx.com:3000/fourbroad/octopus-release/raw/branch/dev/install.ps1 | iex; Install-Octopus -Version 0.1.3
#>
[CmdletBinding()]
param(
[string]$Version = "",
[switch]$NoModifyPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$APP = "octopus"
$REPO = "fourbroad/octopus-release"
$baseUrl = if ($env:OCTOPUS_DOWNLOAD_BASE) { $env:OCTOPUS_DOWNLOAD_BASE } else { "http://octopus.51zxtx.com:3000" }
$INSTALL_DIR = Join-Path $env:USERPROFILE ".octopus\bin"
function Write-Color([string]$Text, [string]$Color = "White") {
Write-Host $Text -ForegroundColor $Color
}
function Get-Arch {
$arch = $env:PROCESSOR_ARCHITECTURE
if ($arch -eq "AMD64") { return "x64" }
if ($arch -eq "ARM64") { return "arm64" }
throw "Unsupported architecture: $arch"
}
function Test-Avx2Support {
try {
Add-Type -MemberDefinition @"
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);
"@ -Name "Kernel32" -Namespace "Win32" -ErrorAction SilentlyContinue | Out-Null
# PF_AVX2_INSTRUCTIONS_AVAILABLE = 40
return [Win32.Kernel32]::IsProcessorFeaturePresent(40)
} catch {
return $false
}
}
function Get-LatestVersion {
# Resolve the latest version by following the /releases/latest redirect to
# /releases/tag/vX.Y.Z. This avoids the API, which is
# rate-limited to 60 requests/hour per IP.
$latestUrl = "$baseUrl/$REPO/releases/latest"
$location = $null
$request = [System.Net.HttpWebRequest]::Create($latestUrl)
$request.Method = "HEAD"
$request.AllowAutoRedirect = $false
$request.UserAgent = "octopus-installer"
try {
$response = $request.GetResponse()
$location = $response.Headers["Location"]
$response.Close()
} catch [System.Net.WebException] {
$resp = $_.Exception.Response
if ($null -ne $resp) {
$location = $resp.Headers["Location"]
$resp.Close()
}
}
if (-not $location) { return "" }
if ($location -match '/tag/v?([^/]+)/?$') {
return $Matches[1] -replace '^v', ''
}
return ""
}
function Install-Octopus {
$arch = Get-Arch
# Resolve version
if ($Version -ne "") {
$specificVersion = $Version -replace '^v', ''
# Verify release exists
$releaseUrl = "$baseUrl/$REPO/releases/tag/v$specificVersion"
try {
$null = Invoke-WebRequest -Uri $releaseUrl -Method Head -UseBasicParsing -ErrorAction Stop
} catch {
Write-Color "Error: Release v$specificVersion not found." "Red"
Write-Color "Available releases: $baseUrl/$REPO/releases" "DarkGray"
exit 1
}
} else {
Write-Color "Fetching latest version..." "DarkGray"
$specificVersion = Get-LatestVersion
if (-not $specificVersion) {
Write-Color "Error: Failed to fetch latest version." "Red"
exit 1
}
}
# Check if already installed at this version
$existing = Join-Path $INSTALL_DIR "$APP.exe"
if (Test-Path $existing) {
try {
$installedVersion = & $existing --version 2>$null
if ($installedVersion -eq $specificVersion) {
Write-Color "Version $specificVersion already installed." "DarkGray"
exit 0
} else {
Write-Color "Installed version: $installedVersion" "DarkGray"
}
} catch { }
}
# Build filename
$target = "$APP-windows-$arch"
if ($arch -eq "x64" -and -not (Test-Avx2Support)) {
$target = "$APP-windows-$arch-baseline"
}
$filename = "$target.zip"
$downloadUrl = "$baseUrl/$REPO/releases/download/v$specificVersion/$filename"
Write-Color ""
Write-Color "Installing octopus version: $specificVersion" "DarkGray"
Write-Color "Downloading $filename ..." "DarkGray"
# Download
$tmpDir = Join-Path $env:TEMP "octopus_install_$PID"
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
$zipPath = Join-Path $tmpDir $filename
try {
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing
} catch {
Write-Color "Error: Failed to download $downloadUrl" "Red"
Write-Color $_.Exception.Message "Red"
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
exit 1
}
# Extract
Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force
# Install
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
$exeSrc = Join-Path $tmpDir "$APP.exe"
if (-not (Test-Path $exeSrc)) {
Write-Color "Error: $APP.exe not found in archive." "Red"
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
exit 1
}
Copy-Item -Path $exeSrc -Destination (Join-Path $INSTALL_DIR "$APP.exe") -Force
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
# Add to PATH
if (-not $NoModifyPath) {
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($userPath -notlike "*$INSTALL_DIR*") {
[Environment]::SetEnvironmentVariable("PATH", "$INSTALL_DIR;$userPath", "User")
Write-Color "Added $INSTALL_DIR to user PATH." "DarkGray"
Write-Color "Restart your terminal for PATH changes to take effect." "Yellow"
}
}
# CI Actions support (GitHub/Gitea)
if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) {
Add-Content -Path $env:GITHUB_PATH -Value $INSTALL_DIR
Write-Color "Added $INSTALL_DIR to GITHUB_PATH." "DarkGray"
}
Write-Host ""
Write-Host " ___ _" -ForegroundColor DarkYellow
Write-Host " / _ \ ___| |_ ___ _ __ _ _ ___" -ForegroundColor DarkYellow
Write-Host "| | | |/ __| __/ _ \| '_ \| | | / __|" -ForegroundColor Yellow
Write-Host "| |_| | (__| || (_) | |_) | |_| \__ \" -ForegroundColor Yellow
Write-Host " \___/ \___|\__\___/| .__/ \__,_|___/" -ForegroundColor White
Write-Host " |_|" -ForegroundColor White
Write-Host ""
Write-Host "Octopus includes free models, to start:" -ForegroundColor DarkGray
Write-Host ""
Write-Host " cd <project> " -NoNewline; Write-Host "# Open directory" -ForegroundColor DarkGray
Write-Host " octopus " -NoNewline; Write-Host "# Run command" -ForegroundColor DarkGray
Write-Host ""
Write-Host "For more information visit " -NoNewline -ForegroundColor DarkGray
Write-Host "https://octopus.51zxtx.com/docs"
Write-Host ""
}
Install-Octopus
+75
View File
@@ -0,0 +1,75 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Regression tests for install.ps1.
.BUG
`irm <url> | iex` closed the terminal window. Cause: the script used
`exit`, which under Invoke-Expression terminates the entire PowerShell
host process (there is no script scope to exit). Fix: use `return`
(success) / `throw` (errors) so the host survives.
.NOTES
Host-process termination cannot be observed from inside the dying process,
so these tests combine a source-verification guard with a behavioral test
run in a child process (`exit` cannot be caught by try/catch and kills the
child; `throw` is catchable).
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$installScript = Join-Path $repoRoot "install.ps1"
$failures = [System.Collections.Generic.List[string]]::new()
function Assert-True([bool]$Cond, [string]$Message) {
if (-not $Cond) { $failures.Add($Message) | Out-Null }
}
# --- Test 1 (source verification): no host-terminating `exit` ---
$content = Get-Content -Path $installScript -Raw
$exitCount = ([regex]::Matches($content, '(?m)^\s*exit\b')).Count
Assert-True ($exitCount -eq 0) "install.ps1 must not use bare 'exit' (found $exitCount); 'irm | iex' would kill the host. Use return/throw."
# --- Test 2: parses cleanly ---
$parseOk = $true
try { [void][scriptblock]::Create($content) } catch { $parseOk = $false }
Assert-True $parseOk "install.ps1 must parse without syntax errors."
# --- Test 3 (behavioral): iex must NOT kill the host ---
# Force the version lookup to fail fast against an unreachable address so the
# script reaches its early-exit/throw path offline. `exit` terminates the child
# (RESULT never emitted); `throw` is caught and RESULT is emitted.
$inner = @'
$ErrorActionPreference = "Stop"
$env:OCTOPUS_DOWNLOAD_BASE = "http://127.0.0.1:1"
$r = "nothing"
try {
Invoke-Expression (Get-Content -Raw "__INSTALL__")
$r = "completed"
} catch {
$r = "caught-throw"
}
Write-Output ("RESULT=$r")
'@.Replace("__INSTALL__", $installScript)
$tmp = Join-Path $env:TEMP ("octopus_iex_test_" + $PID + ".ps1")
Set-Content -Path $tmp -Value $inner -Encoding UTF8
try {
$childOut = (& powershell -NoProfile -File $tmp 2>&1 | Out-String)
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
Assert-True ($childOut -match 'RESULT=') "iex must not terminate the host (try/catch must observe a RESULT). Child output:`n$childOut"
# --- Report ---
if ($failures.Count -gt 0) {
Write-Host ("FAIL: " + $failures.Count + " test(s)") -ForegroundColor Red
foreach ($f in $failures) { Write-Host (" - " + $f) -ForegroundColor Red }
exit 1
}
Write-Host "PASS: all regression tests passed." -ForegroundColor Green
exit 0