Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: 🔍 Pre-commit hooks
uses: pre-commit/action@v3.0.1

- name: 🧪 install.sh unit tests
run: sh dev/install/install_test.sh

feature-check:
# Guards the library/CLI cleave: the `cli` feature gates clap, skim,
# crossterm, termimad, env_logger, humantime. If anything reachable from
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ Alternatively, disable Windows Terminal's alias (Settings → Privacy & security
sudo pacman -S worktrunk && wt config shell install
```

<details>
<summary><strong>Script installer (experimental)</strong></summary>

Downloads a static binary — no package manager required.

**macOS & Linux:**

```bash
curl -fsSL https://worktrunk.dev/install.sh | sh
```

**Windows:**

```bash
powershell -c "irm https://worktrunk.dev/install.ps1 | iex"
```

</details>

## Quick start

Create a worktree for a new feature:
Expand Down
165 changes: 165 additions & 0 deletions dev/install/install_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/bin/sh
set -eu

# Unit tests for docs/static/install.sh.
#
# These are fast behavioral tests that run install.sh with a mocked curl on
# PATH and verify exit codes + output. They cover the error/edge paths only.
# The happy path (curl | sh yielding a working `wt`) is covered by the
# container hand-test: dev/install/test-containers.sh

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh"

PASS=0
FAIL=0

pass() {
PASS=$((PASS + 1))
echo " PASS: $1"
}

fail() {
FAIL=$((FAIL + 1))
echo " FAIL: $1"
if [ -n "${2:-}" ]; then
printf ' %s\n' "$2"
fi
}

MOCK_DIR="$(mktemp -d)"
CARGO_DIR="$(mktemp -d)"
trap 'rm -rf "$MOCK_DIR" "$CARGO_DIR"' EXIT

# Write an executable shell script to $MOCK_DIR/$1 with the given body.
mock_bin() {
name="$1"
body="$2"
path="$MOCK_DIR/$name"
printf '#!/bin/sh\n%s\n' "$body" > "$path"
chmod +x "$path"
}

# Mock curl to find the `-o <path>` arg and write a minimal installer there
# that exits with the given code. Simulates a successful download of an
# installer whose *execution* then succeeds ($1=0) or fails ($1!=0).
mock_curl_writes_installer() {
mock_bin curl "while [ \$# -gt 0 ]; do
if [ \"\$1\" = \"-o\" ]; then
shift
printf '%s\n' '#!/bin/sh' 'exit $1' > \"\$1\"
exit 0
fi
shift
done
exit 1"
}

# Run install.sh with $MOCK_DIR first on PATH and a curated environment.
# Captures exit code in $rc and combined output in $output. Pass a value for
# $OS as the first argument (default empty).
run_install() {
os_val="${1:-}"
set +e
output="$(PATH="$MOCK_DIR:/usr/bin:/bin" \
OS="$os_val" \
HOME="$CARGO_DIR/home" \
CARGO_HOME="$CARGO_DIR/cargo" \
sh "$INSTALL_SH" 2>&1)"
rc=$?
set -e
}

echo "=== install.sh test suite ==="
echo ""

# ---------------------------------------------------------------------------
echo "--- Platform gate ---"

# Windows detection: sets OS=Windows_NT, expects exit 1 with guidance.
run_install Windows_NT
if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected" \
&& echo "$output" | grep -q "install.ps1"; then
pass "exits with PowerShell guidance when OS=Windows_NT"
else
fail "exits with PowerShell guidance when OS=Windows_NT" "rc=$rc output: $output"
fi

echo ""

# ---------------------------------------------------------------------------
echo "--- Download / installer failures ---"

# curl fails to download: expect non-zero exit.
mock_bin curl 'exit 22'
run_install
if [ "$rc" -ne 0 ]; then
pass "exits non-zero when curl fails"
else
fail "exits non-zero when curl fails" "rc=$rc output: $output"
fi

# curl succeeds but writes a failing installer: expect non-zero exit.
mock_curl_writes_installer 5
run_install
if [ "$rc" -ne 0 ]; then
pass "exits non-zero when upstream installer fails"
else
fail "exits non-zero when upstream installer fails" "rc=$rc output: $output"
fi

echo ""

# ---------------------------------------------------------------------------
echo "--- Post-install path resolution ---"

# curl writes a no-op installer; CARGO_HOME is empty and wt is absent from
# PATH → script should warn and exit 0 (installer succeeded but wt missing).
mock_curl_writes_installer 0
mkdir -p "$CARGO_DIR/cargo"
run_install
if [ "$rc" -eq 0 ] && echo "$output" | grep -q "'wt' not found in PATH"; then
pass "warns and exits 0 when wt is missing after install"
else
fail "warns and exits 0 when wt is missing after install" "rc=$rc output: $output"
fi

# As above, but wt exists in CARGO_HOME/bin with an env file. The script
# should source env, find wt, and reach the post-wt-found stage. Whether the
# final `wt config shell install` actually runs depends on /dev/tty being
# openable — that's a property of the test environment, so accept either the
# sentinel (TTY) or the non-interactive fallback message (no TTY).
sentinel="$CARGO_DIR/wt.args"
mkdir -p "$CARGO_DIR/cargo/bin"
cat > "$CARGO_DIR/cargo/bin/wt" <<WT
#!/bin/sh
printf '%s\n' "\$*" > "$sentinel"
WT
chmod +x "$CARGO_DIR/cargo/bin/wt"
cat > "$CARGO_DIR/cargo/env" <<ENV
export PATH="$CARGO_DIR/cargo/bin:\$PATH"
ENV
run_install
reached_post_wt=false
if [ -f "$sentinel" ] && grep -q "config shell install" "$sentinel"; then
reached_post_wt=true
elif echo "$output" | grep -q "Non-interactive environment"; then
reached_post_wt=true
fi
if [ "$rc" -eq 0 ] && [ "$reached_post_wt" = true ]; then
pass "finds wt on PATH after install and reaches shell-install stage"
else
fail "finds wt on PATH after install and reaches shell-install stage" \
"rc=$rc sentinel=$(cat "$sentinel" 2>/dev/null) output: $output"
fi

echo ""

# ---------------------------------------------------------------------------
echo "=== Results ==="
echo " $PASS passed, $FAIL failed"
echo ""

if [ "$FAIL" -gt 0 ]; then
exit 1
fi
101 changes: 101 additions & 0 deletions dev/install/test-containers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/sh
set -eu

# Hand-test: run docs/static/install.sh inside clean Docker containers and
# verify it produces a working `wt`. Not wired into CI because it hits the
# real network (GitHub releases) and needs Docker.
#
# Usage:
# sh dev/install/test-containers.sh # test all images
# sh dev/install/test-containers.sh ubuntu # test one image
#
# The script under test is the one checked into this repo (not fetched from
# worktrunk.dev) — we're testing the current source, not what's published.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh"

if ! command -v docker >/dev/null 2>&1; then
echo "docker is required for the container test. Install Docker Desktop or"
echo "equivalent, then re-run."
exit 1
fi

# Each entry: image | setup. The upstream cargo-dist installer downloads a
# tar.xz release and extracts it, so `xz` must be on PATH alongside curl.
IMAGES="
ubuntu:24.04|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils
debian:12|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils
fedora:41|dnf install -y -q curl xz
alpine:3.20|apk add --no-cache curl ca-certificates xz
archlinux:latest|pacman -Sy --noconfirm curl ca-certificates xz
"

FILTER="${1:-}"
PASS=0
FAIL=0
FAILED=""

run_one() {
image="$1"
setup="$2"

echo ""
echo "=== $image ==="

# Copy install.sh into the container, run setup + install, then verify
# `wt --version` prints a worktrunk version. Use `sh -c` as entrypoint so
# the script runs regardless of the image's default command. We capture
# into a temp file rather than piping — a pipe would mask docker's exit
# code behind sed's.
log="$(mktemp)"
set +e
docker run --rm \
-v "$INSTALL_SH:/tmp/install.sh:ro" \
"$image" \
sh -c "set -e; $setup >/dev/null; sh /tmp/install.sh; . \${CARGO_HOME:-\$HOME/.cargo}/env; wt --version" \
>"$log" 2>&1
rc=$?
set -e
sed 's/^/ /' "$log"
rm -f "$log"

if [ "$rc" -eq 0 ]; then
echo " PASS: $image"
PASS=$((PASS + 1))
else
echo " FAIL: $image (exit $rc)"
FAIL=$((FAIL + 1))
FAILED="$FAILED $image"
fi
}

echo "Testing install.sh in containers..."

# Shell-splitting on newlines in POSIX sh: set IFS to newline, iterate.
old_ifs="$IFS"
IFS='
'
for entry in $IMAGES; do
IFS='|'
# shellcheck disable=SC2086
set -- $entry
IFS="$old_ifs"
image="$1"
setup="$2"
if [ -n "$FILTER" ] && ! echo "$image" | grep -q "$FILTER"; then
continue
fi
run_one "$image" "$setup"
IFS='
'
done
IFS="$old_ifs"

echo ""
echo "=== Results ==="
echo " $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo " Failed:$FAILED"
exit 1
fi
19 changes: 19 additions & 0 deletions docs/content/worktrunk.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ Alternatively, disable Windows Terminal's alias (Settings → Privacy & security

{{ terminal(cmd="sudo pacman -S worktrunk && wt config shell install") }}

<details>
<summary><strong>Script installer (experimental)</strong></summary>

Downloads a static binary — no package manager required.

**macOS & Linux:**

```bash
curl -fsSL https://worktrunk.dev/install.sh | sh
```

**Windows:**

```bash
powershell -c "irm https://worktrunk.dev/install.ps1 | iex"
```

</details>

## Quick start

Create a worktree for a new feature:
Expand Down
42 changes: 42 additions & 0 deletions docs/static/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Worktrunk Installer (Windows)
# https://worktrunk.dev/install.ps1

$ErrorActionPreference = 'Stop'

if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) {
Write-Host "Non-Windows environment detected. Please use the shell installer instead:" -ForegroundColor Yellow
Write-Host " curl -fsSL https://worktrunk.dev/install.sh | sh"
exit 1
}

Write-Host "Installing worktrunk..."
irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.ps1 | iex

# Update PATH to pick up the newly installed binary.
# Respect CARGO_HOME if set, otherwise use the default location.
$cargoBin = if ($env:CARGO_HOME) { "$env:CARGO_HOME\bin" } else { "$HOME\.cargo\bin" }
if ($env:Path -notlike "*$cargoBin*") {
$env:Path += ";$cargoBin"
}

# Check whether `wt` on PATH is actually worktrunk (Windows Terminal uses
# the same alias). Wrap in try/catch — with ErrorActionPreference='Stop',
# a failing `wt --version` would throw instead of falling through.
$wtIsWorktrunk = $false
if (Get-Command wt -ErrorAction SilentlyContinue) {
try {
$wtIsWorktrunk = [bool](wt --version 2>&1 | Select-String 'worktrunk')
} catch {
$wtIsWorktrunk = $false
}
}

if ($wtIsWorktrunk) {
wt config shell install
} elseif (Get-Command git-wt -ErrorAction SilentlyContinue) {
git-wt config shell install
} else {
Write-Host ""
Write-Host "Warning: worktrunk installed but neither 'wt' nor 'git-wt' found in PATH." -ForegroundColor Yellow
Write-Host "Restart your shell and run 'wt config shell install' (or 'git-wt config shell install') manually."
}
Loading
Loading