Skip to content
Merged
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
82 changes: 81 additions & 1 deletion .github/workflows/installer-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ on:
branches: [main]
paths:
- scripts/install.sh
- scripts/install.ps1

permissions: {}

# The post-install skill step reaches for the OS keyring; on a headless macOS
# runner the `security` tool blocks forever on the locked keychain (caught by
# this canary's first run). Same escape hatch the Test workflow uses.
# this canary's first run). Also keeps the Windows legs out of Credential
# Manager. Same escape hatch the Test workflow uses.
env:
BASECAMP_NO_KEYRING: "1"

Expand Down Expand Up @@ -77,3 +79,81 @@ jobs:
curl -fsSL "https://basecamp.com/install-cli?sha=${GITHUB_SHA}" | /bin/bash

"$BASECAMP_BIN_DIR/basecamp" version

windows:
name: Windows (${{ matrix.shell }})
runs-on: windows-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# powershell (5.1) is the stock interpreter the README's `irm | iex`
# shape targets and exercises install.ps1's 5.1-specific branches;
# pwsh covers PowerShell 7.
shell: [powershell, pwsh]
steps:
- name: Install from the published endpoint
# GitHub Actions does not evaluate expressions in steps[*].shell, so a
# pwsh launcher step invokes the matrix interpreter explicitly. The
# documented `irm | iex` shape — fetch and execution both — runs
# entirely inside the target interpreter; the launcher only sets env
# (inherited by the child) and asserts afterwards.
shell: pwsh
env:
SMOKE_SHELL: ${{ matrix.shell }}
run: |
# Scratch USERPROFILE + explicit BASECAMP_BIN_DIR contain the
# installer's and the child basecamp.exe's writes (Go os.UserHomeDir
# reads USERPROFILE). Ensure-UserPath's HKCU PATH write is
# unavoidable but disposable on ephemeral runners.
$smokeRoot = Join-Path $env:RUNNER_TEMP 'installer-smoke'
New-Item -ItemType Directory -Force -Path (Join-Path $smokeRoot 'home') | Out-Null
New-Item -ItemType Directory -Force -Path (Join-Path $smokeRoot 'bin') | Out-Null
$env:USERPROFILE = Join-Path $smokeRoot 'home'
$env:BASECAMP_BIN_DIR = Join-Path $smokeRoot 'bin'
$env:BASECAMP_SKIP_SETUP = '1'
$env:BASECAMP_SETUP_AGENT = 'none'

# Documented Quick Start shape, cache-busted: raw.githubusercontent.com
# varies its cache key on unknown query params, so ?sha= keeps each
# commit's request cache-distinct (no basecamp.com endpoint exists
# for the PowerShell installer). Single-quoted so the inner
# interpreter expands $env:GITHUB_SHA itself.
& $env:SMOKE_SHELL -NoProfile -Command 'irm "https://raw.githubusercontent.com/basecamp/basecamp-cli/main/scripts/install.ps1?sha=$env:GITHUB_SHA" | iex'
if ($LASTEXITCODE -ne 0) { throw "installer exited $LASTEXITCODE under $env:SMOKE_SHELL" }

& "$env:BASECAMP_BIN_DIR\basecamp.exe" version
if ($LASTEXITCODE -ne 0) { throw "basecamp version exited $LASTEXITCODE" }

notify:
name: Notify on failure
needs: [linux, macos, windows]
if: failure()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
steps:
- name: File or update canary failure issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT_NAME: ${{ github.event_name }}
LINUX_RESULT: ${{ needs.linux.result }}
MACOS_RESULT: ${{ needs.macos.result }}
WINDOWS_RESULT: ${{ needs.windows.result }}
run: |
TITLE="Installer canary failure"
BODY="The installer canary failed (event: ${EVENT_NAME}). Legs: linux=${LINUX_RESULT}, macos=${MACOS_RESULT}, windows=${WINDOWS_RESULT}. See ${RUN_URL} for details."

# Check for existing open issue before creating a new one
existing=$(gh issue list --state open --search "in:title $TITLE" --json number,title --jq '[.[] | select(.title == "'"$TITLE"'")][0].number // empty' 2>/dev/null || true)
if [ -n "$existing" ]; then
gh issue comment "$existing" --body "$BODY" || true
else
gh issue create --title "$TITLE" --body "$BODY" || true
fi

# Always emit annotation so the failure is visible in the workflow summary
echo "::error::Installer canary failed. See ${RUN_URL}"
140 changes: 140 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,146 @@ jobs:
fi
make skill-eval

installer-bash32:
name: Installer (bash 3.2)
runs-on: macos-latest
timeout-minutes: 10
permissions:
contents: read
Comment thread
jeremy marked this conversation as resolved.
pull-requests: read # dorny/paths-filter enumerates PR files via the REST API
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Check for installer changes
id: filter
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
# test.yml is in its own filter so the job proves itself on the PR
# that introduces or edits it.
filters: |
installer:
- 'scripts/install.sh'
- 'e2e/installer.bats'
- '.github/workflows/test.yml'

- name: Assert /bin/bash is Bash 3.2
if: steps.filter.outputs.installer == 'true'
run: |
# Same guard as the installer-smoke canary: honest 3.2 coverage
# instead of silently degrading if a future runner image changes
# /bin/bash. The e2e bats suite cannot provide this — bats 1.11.0
# execs `env bash` at every layer, so the interpreter is not
# pinnable through it.
/bin/bash -c '[[ "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" == "3.2" ]]' \
|| { echo "/bin/bash is not Bash 3.2:" >&2; /bin/bash --version >&2; exit 1; }

- name: Syntax-check installer under Bash 3.2
if: steps.filter.outputs.installer == 'true'
run: /bin/bash -n scripts/install.sh

- name: Piped execution under Bash 3.2
if: steps.filter.outputs.installer == 'true'
run: |
# PATH=/nonexistent forces the curl guard: the run must fail with
# the guard's own message, never with an unbound-variable error
# (the #558 regression class). A bare out=$(...) assignment of the
# failing pipeline would exit this step immediately under the
# runner's `bash -e -o pipefail`, so capture via if-else.
status=0
if out=$(cat scripts/install.sh | PATH=/nonexistent /bin/bash 2>&1); then
Comment thread
jeremy marked this conversation as resolved.
status=0
else
status=$?
fi
printf '%s\n' "$out"
if [ "$status" -eq 0 ]; then
echo "expected the curl guard to fail without curl on PATH" >&2
exit 1
fi
printf '%s\n' "$out" | grep -q 'curl is required but not installed'
if printf '%s\n' "$out" | grep -q 'unbound variable'; then
echo "installer emitted an unbound variable error under Bash 3.2" >&2
exit 1
fi

- name: Offline full install under Bash 3.2
if: steps.filter.outputs.installer == 'true'
run: |
# The curl-guard step above never gets past the prerequisite check,
# and `bash -n` only checks grammar — neither catches Bash-4-only
# constructs (declare -A, mapfile, 4.x expansions) in the download,
# checksum, extract, or post-install code. Run one complete install
# under real 3.2: a fixture curl serves a local release from disk,
# and a pinned BASECAMP_VERSION skips version resolution. PATH is
# restricted to fixture + system dirs so Homebrew tools (bash 4+,
# cosign) cannot leak in.
fixture_root=$(mktemp -d)
mkdir -p "$fixture_root/bin" "$fixture_root/release" "$fixture_root/home" "$fixture_root/install"

cat > "$fixture_root/release/basecamp" <<'STUB'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "basecamp version 9.9.9"; fi
exit 0
STUB
chmod +x "$fixture_root/release/basecamp"
for arch in arm64 amd64; do
tar -czf "$fixture_root/release/basecamp_9.9.9_darwin_${arch}.tar.gz" -C "$fixture_root/release" basecamp
done
(cd "$fixture_root/release" && shasum -a 256 ./*.tar.gz | sed 's|\./||' > checksums.txt)

cat > "$fixture_root/bin/curl" <<'STUB'
#!/bin/bash
# Fixture curl: serves the local release dir. Handles the two shapes
# install.sh uses: `--version` and `[-flags] <url> -o <dest>`.
out=""; url=""
while [ $# -gt 0 ]; do
case "$1" in
--version) echo "curl 8.0.0 (fixture)"; exit 0 ;;
-o) out="$2"; shift ;;
http://*|https://*) url="$1" ;;
esac
shift
done
name="${url##*/}"
if [ -n "$out" ] && [ -f "$FIXTURE_RELEASE_DIR/$name" ]; then
cp "$FIXTURE_RELEASE_DIR/$name" "$out"
exit 0
fi
echo "fixture curl: no fixture for: $url" >&2
exit 22
STUB
chmod +x "$fixture_root/bin/curl"

status=0
if out=$(cat scripts/install.sh | \
HOME="$fixture_root/home" \
FIXTURE_RELEASE_DIR="$fixture_root/release" \
BASECAMP_BIN_DIR="$fixture_root/install" \
BASECAMP_VERSION=9.9.9 \
BASECAMP_SKIP_SETUP=1 \
BASECAMP_SETUP_AGENT=none \
PATH="$fixture_root/bin:/usr/bin:/bin" \
/bin/bash 2>&1); then
status=0
else
status=$?
fi
printf '%s\n' "$out"
if [ "$status" -ne 0 ]; then
echo "offline install failed under Bash 3.2 (exit $status)" >&2
exit 1
fi
printf '%s\n' "$out" | grep -q 'Installed basecamp to'
printf '%s\n' "$out" | grep -q 'version 9.9.9 installed'
if printf '%s\n' "$out" | grep -q 'unbound variable'; then
echo "installer emitted an unbound variable error under Bash 3.2" >&2
exit 1
fi
"$fixture_root/install/basecamp" --version

benchmarks:
name: Benchmarks
runs-on: ubuntu-latest
Expand Down
Loading