Skip to content

fix: make Bash tests platform-explicit #14

fix: make Bash tests platform-explicit

fix: make Bash tests platform-explicit #14

Workflow file for this run

name: CI
on:
push:
branches: [main, master]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
# The important one: prove that a clean machine can install multi-cli from
# scratch and immediately run its core commands. This is the check that would
# have caught the install failure (missing jq / broken launcher).
install-smoke:
name: install + run (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
# Force the installer's own jq-resolution path: both runner images ship
# jq pre-installed, so we remove it first. The installer must put jq back
# (apt-get on Linux, brew on macOS) or the whole CLI is dead on arrival.
- name: Remove pre-installed jq so the installer must provide it
run: |
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get remove -y jq || true
elif command -v brew >/dev/null 2>&1; then
brew uninstall --ignore-dependencies --force jq || true
fi
hash -r
if command -v jq >/dev/null 2>&1; then
echo "WARNING: jq still present at $(command -v jq); installer will detect-and-skip."
else
echo "jq removed; installer must reinstall it."
fi
- name: Install multi-cli from the working tree
run: bash scripts/install.sh --local
- name: jq is available after install
run: |
command -v jq
jq --version
# The launcher lands in ~/.local/bin (see install.sh BIN_LINK default).
- name: doctor / tools / new must all exit 0
run: |
export PATH="$HOME/.local/bin:$PATH"
multi-cli doctor
multi-cli tools
multi-cli new codex/ciprofile
# Shell correctness for every .sh and the main launcher. Run at error
# severity: the current tree has only style-level warnings (SC2155 "declare
# and assign separately" x24, SC2115 x1 in `multi-cli`) and zero errors, so
# error severity keeps CI green while still failing on real bugs (parse
# errors, undefined behavior, CRLF/SC1017, bad redirects, etc.).
shellcheck:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
- name: Lint shell scripts
run: shellcheck --severity=error multi-cli scripts/install.sh scripts/uninstall.sh
# PowerShell static analysis for the Windows scripts. Run at Error severity
# and exclude the rules that flag accepted design choices, not bugs:
# PSAvoidUsingConvertToSecureStringWithPlainText (Error-severity, fires on
# multi-cli.ps1's credential handling) and PSUseShouldProcessForStateChanging-
# Functions. Excluding these keeps CI green while still catching genuine
# parse/syntax failures and other error-severity rules.
pwsh-lint:
name: PSScriptAnalyzer
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install PSScriptAnalyzer
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser
- name: Analyze PowerShell scripts
shell: pwsh
run: |
$targets = @(
'scripts/install.ps1',
'scripts/uninstall.ps1',
'multi-cli.ps1'
)
$excludedRules = @(
'PSAvoidUsingConvertToSecureStringWithPlainText',
'PSUseShouldProcessForStateChangingFunctions'
)
$findings = foreach ($target in $targets) {
Invoke-ScriptAnalyzer -Path $target -Severity Error -ExcludeRule $excludedRules
}
if ($findings) {
$findings | Format-Table -AutoSize | Out-String | Write-Host
throw "PSScriptAnalyzer reported $($findings.Count) error-severity finding(s)."
}
Write-Host "No error-severity findings."
# The bash session-continuation suite (bats). Runs the REAL launcher against
# real fixture trees on both Linux and macOS. bats-core and jq are fetched
# fresh by the runner (never committed): jq via the system package manager,
# bats-core via the system package manager when available, otherwise
# tests/run-bats.sh clones the pinned v1.11.0 into tests/vendor on demand.
bats:
name: bats (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Install bats + jq
run: |
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
# libsecret-tools + gnome-keyring + dbus-x11: the auth/processSecret
# tests exercise the real Linux Secret Service backend (secret-tool),
# which needs both the CLI and a running keyring daemon.
sudo apt-get install -y bats jq libsecret-tools gnome-keyring dbus-x11 acl
elif command -v brew >/dev/null 2>&1; then
brew install bats-core jq
else
echo "no supported package manager (apt-get/brew) on this runner" >&2
exit 1
fi
bats --version
jq --version
- name: Validate adapter contracts
run: bash scripts/validate-adapters.sh
# macOS: `security` against the default login keychain can block on a GUI
# unlock/access prompt on a headless runner, hanging the whole suite.
# Give the run a dedicated ephemeral keychain with a known password,
# unlocked, partition-listed, and made default so no prompt can appear.
- name: Prepare an ephemeral CI keychain (macOS)
if: runner.os == 'macOS'
run: |
KEYCHAIN="multicli-ci-$(uuidgen).keychain"
KEYCHAIN_PASSWORD="multicli-ci"
PREVIOUS_DEFAULT="$(security default-keychain -d user | tr -d '"' | xargs)"
echo "MULTICLI_CI_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
echo "MULTICLI_PREVIOUS_DEFAULT_KEYCHAIN=$PREVIOUS_DEFAULT" >> "$GITHUB_ENV"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security default-keychain -d user -s "$KEYCHAIN"
# shellcheck disable=SC2046
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
- name: Run the bats suite
run: |
if [ "$(uname -s)" = "Linux" ]; then
# Secret Service needs a session bus and an unlocked keyring.
# Smoke-check the store first so a broken keyring setup fails here
# with a clear error instead of obscure test failures.
dbus-run-session -- bash -c '
set -euo pipefail
printf "\n" | gnome-keyring-daemon --unlock --components=secrets >/dev/null
printf "ci-smoke" | secret-tool store --label=ci-smoke service multicli-ci target smoke
[ "$(secret-tool lookup service multicli-ci target smoke)" = "ci-smoke" ]
secret-tool clear service multicli-ci target smoke
exec bash tests/run-bats.sh
'
else
env MULTICLI_MACOS_KEYCHAIN="$HOME/Library/Keychains/$MULTICLI_CI_KEYCHAIN-db" bash tests/run-bats.sh
fi
- name: Run the real POSIX OS-user lifecycle
timeout-minutes: 10
run: bash tests/e2e/posix-osuser.sh
- name: Remove the ephemeral CI keychain (macOS)
if: runner.os == 'macOS' && always()
run: |
if [ -n "${MULTICLI_PREVIOUS_DEFAULT_KEYCHAIN:-}" ]; then
security default-keychain -d user -s "$MULTICLI_PREVIOUS_DEFAULT_KEYCHAIN"
fi
if [ -n "${MULTICLI_CI_KEYCHAIN:-}" ]; then
security delete-keychain "$MULTICLI_CI_KEYCHAIN"
fi
bash-coverage:
name: bash coverage (ubuntu-latest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install coverage dependencies
run: |
KCOV_VERSION=v43
KCOV_SHA256=4cbba86af11f72de0c7514e09d59c7927ed25df7cebdad087f6d3623213b95bf
KCOV_ARCHIVE="$RUNNER_TEMP/kcov-source.tar.gz"
KCOV_SOURCE="$RUNNER_TEMP/kcov-source"
sudo apt-get update -y
sudo apt-get install -y bats jq libsecret-tools gnome-keyring dbus-x11 acl \
binutils-dev build-essential cmake libcurl4-openssl-dev libdw-dev \
libelf-dev libiberty-dev libssl-dev ninja-build zlib1g-dev
curl -fsSL "https://github.com/SimonKagstrom/kcov/archive/refs/tags/$KCOV_VERSION.tar.gz" -o "$KCOV_ARCHIVE"
printf '%s %s\n' "$KCOV_SHA256" "$KCOV_ARCHIVE" | sha256sum --check --strict
mkdir -p "$KCOV_SOURCE"
tar -xzf "$KCOV_ARCHIVE" -C "$KCOV_SOURCE" --strip-components=1
cmake -S "$KCOV_SOURCE" -B "$KCOV_SOURCE/build" -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build "$KCOV_SOURCE/build"
sudo cmake --install "$KCOV_SOURCE/build"
kcov --version
- name: Enforce Bash coverage
env:
COVERAGE_BASELINE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD^' }}
run: |
dbus-run-session -- bash -c '
set -euo pipefail
printf "\n" | gnome-keyring-daemon --unlock --components=secrets >/dev/null
exec env COVERAGE_BASELINE="$COVERAGE_BASELINE" bash tests/coverage/run-bash-coverage.sh
'
# The PowerShell session-continuation suite (Pester). Uses the Pester 3.4
# shipped with Windows PowerShell 5.1 (the suite is 3.4-compatible). The
# PowerShell launcher parses JSON natively, so this suite needs no jq.
pester:
name: pester (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Pester 3.4.0 (matches the 3.x-compatible suite)
shell: powershell
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-PackageProvider -Name NuGet -Force -Scope CurrentUser
Install-Module -Name Pester -RequiredVersion 3.4.0 -Force -SkipPublisherCheck -Scope CurrentUser
- name: Validate adapter contracts
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/Validate-Adapters.ps1
- name: Run the Pester suite
timeout-minutes: 30
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests/run-pester.ps1 -CI
- name: Enforce PowerShell module coverage
shell: powershell
env:
COVERAGE_BASELINE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD^' }}
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests/coverage/Invoke-ModuleCoverage.ps1 -MinimumPercent 95
windows-install-smoke:
name: install + run (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install from working tree
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install.ps1 -Local
- name: Validate installed launcher
shell: powershell
run: |
powershell -NoProfile -ExecutionPolicy Bypass -File multi-cli.ps1 doctor
powershell -NoProfile -ExecutionPolicy Bypass -File multi-cli.ps1 tools