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
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [master]
pull_request:

permissions:
contents: read

jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
# Gating: the actively-maintained installer must stay free of error-level
# findings. setup.sh + functions are error-clean today.
- name: ShellCheck core installer (gating)
run: shellcheck --severity=error setup.sh functions
# Informational: full report incl. migration scripts (which still carry
# error-level findings — see tests/README.md "Known issues"). Non-gating
# until those are triaged; ratchet up once they are fixed.
- name: ShellCheck full report (informational)
run: |
shellcheck --severity=style \
setup.sh functions migration/migrate.sh migration/remote-migrate || true

shfmt:
name: shfmt (informational)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install shfmt
run: sudo apt-get update && sudo apt-get install -y shfmt || true
- name: Show formatting diff
run: |
command -v shfmt >/dev/null 2>&1 || { echo "shfmt unavailable; skipping"; exit 0; }
shfmt -d -i 2 -ci setup.sh functions || true

unit:
name: Unit tests (bats)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install bats
run: sudo apt-get update && sudo apt-get install -y bats
- name: Run unit tests
run: bats tests/unit

integration:
name: Host prep (${{ matrix.image }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image:
- ubuntu:22.04 # LTS
- ubuntu:24.04 # LTS
- ubuntu:26.04 # LTS — pinned: drove the host-PHP fixes, kept after rolling moves on
- ubuntu:rolling # latest stable release (floats forward)
- ubuntu:devel # next, in-development Ubuntu — earliest warning
- debian:12
- debian:13
container:
image: ${{ matrix.image }}
steps:
- name: Install checkout prerequisites
run: |
apt-get update
apt-get install -y --no-install-recommends ca-certificates git
- uses: actions/checkout@v4
- name: Run host-prep assertions
run: bash tests/integration/host-prep.sh
8 changes: 8 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ShellCheck configuration for the EasyEngine installer.

# Follow sourced files where the path is statically known.
external-sources=true

# The installer sources files whose paths are only known at runtime
# (e.g. "$TMP_WORK_DIR/helper-functions"); shellcheck cannot follow these.
disable=SC1090,SC1091
106 changes: 106 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Tests

Automated tests for the EasyEngine installer. Two layers:

| Layer | Location | What it covers | Needs |
|-------|----------|----------------|-------|
| Unit | `tests/unit/*.bats` | Pure-ish logic in `functions`, with all external commands stubbed | [bats-core] |
| Integration | `tests/integration/host-prep.sh` | The host-prep stage run against a distro's **real** apt repos | Docker (or run inside a container) |

CI wires both up plus ShellCheck — see `.github/workflows/ci.yml`.

## Why integration stops before the phar

`do_install()` ends with `pull_easyengine_images`, which runs `ee cli info` and
makes EasyEngine bootstrap its global Docker services (multi-GB image pulls,
a running Docker daemon, migrations). That is EasyEngine-**core** behaviour, not
the installer's, and is slow/flaky to reproduce in CI. So the integration test is
bounded **before** the phar download/pull and stubs `docker`, exercising only the
OS-version–dependent surface that has actually regressed in the past:

```
check_dependencies → setup_host_dependencies → setup_php → setup_php_extensions
```

PHP selection in particular can only be validated against real distro repos +
the real ondrej/sury PPA — a stub can't reproduce "ondrej has no build for this
codename yet", which is exactly the Ubuntu 26.04 class of bug.

## Running

### Unit tests

```bash
# With bats installed locally:
bats tests/unit

# Or via Docker, no local install:
docker run --rm -v "$PWD":/code -w /code bats/bats:latest tests/unit
```

### Integration tests

```bash
# One distro:
docker run --rm -v "$PWD":/installer:ro -w /installer ubuntu:24.04 \
bash tests/integration/host-prep.sh

# Full local matrix (same images as CI):
tests/integration/run-matrix.sh
# ...or a subset:
tests/integration/run-matrix.sh ubuntu:24.04 debian:12
```

### ShellCheck

```bash
shellcheck setup.sh functions migration/migrate.sh migration/remote-migrate
```

## How the unit stubs work

`tests/helpers/common.bash` prepends a temp dir to `PATH` and drops fake
executables into it (`make_stub apt-get <<'EOF' … EOF`), so calls to `apt-get`,
`curl`, `sleep`, etc. hit the stub instead of the host. The PHP-selection test
goes one step further and overrides the four `ee_php_*` helpers as shell
functions to drive the algorithm through every branch deterministically.

## Coverage today

- `parse_args` — every flag, including the `--remote-host` value-consuming case.
- `ee_apt_update` — succeeds first try / retries only on lock contention / aborts
on a non-lock error.
- `get_ondrej_php_ppa_release_status` — 200/404/curl-failure.
- `ee_select_and_install_php` — native vs PPA, version-first ordering, no-candidate.
- `download_and_install_easyengine` — checksum match / mismatch / empty phar /
empty checksum / download failure.
- Host-prep integration — php + the three required extensions install across
Ubuntu 22.04/24.04/26.04/rolling/devel and Debian 12/13.

## Not yet covered (candidate follow-ups)

- **`pull_easyengine_images` / full `setup.sh` end-to-end.** Would need a small
testability seam: an `EE_LOCAL_PHAR` override mirroring the existing
`EE_LOCAL_FUNCTIONS` hook so a fake `ee` can stand in for the real phar (also
useful for air-gapped/mirror installs). Not added here to keep this change
test-only with zero production edits.
- **Idempotency** — running the installer twice and asserting the second run is a
clean no-op. Needs the seam above.
- **`add_ssl_renew_cron`** — cron-entry creation (incl. the no-existing-crontab case).
- **Migration scripts** (`migration/*`) — only ShellCheck'd today; see below.

## Known issues these tests / ShellCheck surface

Found while building the suite; not fixed here (test-only change):

- `--dry-run` is documented in `migration/README.md` and parsed by `parse_args`
(sets `EE_DRY_RUN`), but `EE_DRY_RUN` is read **nowhere** — `ee migrate --dry-run`
performs a real migration.
- `--trace` sets `EE_TRACE`, but the migration scripts check `$TRACE` → the flag
is a no-op. `--all` sets `EE_SITE_ALL`, also unused.
- `migration/migrate.sh:156` references `$new_site_name`, which is never assigned
(the site is created with an empty name).
- `migration/{migrate,remote-migrate}` reference undefined `$migrate`, `$Red`,
`$RCol`, and have 8 error-level `SC2068` unquoted array expansions.

[bats-core]: https://github.com/bats-core/bats-core
37 changes: 37 additions & 0 deletions tests/helpers/common.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Shared helpers for the bats unit suite.
#
# These tests source the real `functions` file and replace external commands
# (apt-get, curl, sleep, …) with stubs on PATH, so the installer's logic can be
# exercised without touching the host or the network.

# Absolute path to the repo root (this file lives in tests/helpers/).
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
export REPO_ROOT

# Create a private dir at the front of PATH to hold command stubs.
stub_init() {
STUB_DIR="$(mktemp -d)"
PATH="$STUB_DIR:$PATH"
export STUB_DIR PATH
}

stub_cleanup() {
[ -n "${STUB_DIR:-}" ] && rm -rf "$STUB_DIR"
}

# make_stub NAME — body is read from stdin and becomes an executable on PATH.
# make_stub apt-get <<'EOF'
# #!/usr/bin/env bash
# exit 0
# EOF
make_stub() {
local p="$STUB_DIR/$1"
cat > "$p"
chmod +x "$p"
}

# Source the installer functions under test.
load_functions() {
# shellcheck source=/dev/null
source "$REPO_ROOT/functions"
}
88 changes: 88 additions & 0 deletions tests/integration/host-prep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Integration test for the installer's host-preparation stage, run INSIDE a
# distro container against that distro's REAL package repositories.
#
# Scope is deliberately bounded *before* the phar download / image pull: those
# steps belong to EasyEngine-core CI, need a working Docker daemon, and pull
# multi-GB images. Here we only prove the part that is OS-version dependent and
# has historically broken (PHP selection across Ubuntu/Debian releases, the
# required PHP extensions, and the base dependencies).
#
# Docker is stubbed so setup_docker() short-circuits — we are not installing or
# running the Docker daemon here.

set -uo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

fails=0
pass() { printf 'ok - %s\n' "$1"; }
fail() { printf 'FAIL - %s\n' "$1"; fails=$((fails + 1)); }
check() { # check "description" cmd...
local desc="$1"; shift
if "$@" >/dev/null 2>&1; then pass "$desc"; else fail "$desc"; fi
}

export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a

# Minimal bootstrap: the functions need lsb_release + an apt cache.
apt-get update -y >/dev/null 2>&1 || true
command -v lsb_release >/dev/null 2>&1 || \
apt-get install -y --no-install-recommends lsb-release ca-certificates curl >/dev/null 2>&1 || true

export EE_LINUX_DISTRO LOG_FILE EE_QUIET_OUTPUT
EE_LINUX_DISTRO="$(lsb_release -i 2>/dev/null | awk '{print $3}')"
LOG_FILE=/dev/null
EE_QUIET_OUTPUT=""

# Stub docker/docker-compose so setup_docker()'s `command -v docker` guard skips
# the daemon install (out of scope for this test).
STUB_DIR="$(mktemp -d)"
for c in docker docker-compose; do
printf '#!/bin/sh\nexit 0\n' > "$STUB_DIR/$c"
chmod +x "$STUB_DIR/$c"
done
export PATH="$STUB_DIR:$PATH"
trap 'rm -rf "$STUB_DIR"' EXIT

# shellcheck source=/dev/null
source "$REPO_ROOT/functions"

echo "==> distro: $(lsb_release -ds 2>/dev/null || echo "$EE_LINUX_DISTRO")"
echo "==> EE_LINUX_DISTRO=$EE_LINUX_DISTRO"
echo

ee_apt_update >/dev/null 2>&1 || true

# --- the stage under test ---
setup_host_dependencies
setup_php
setup_php_extensions

echo
echo "==> assertions"

# The installer's actual promise after this stage:
check "php CLI is installed" command -v php
check "EE_INSTALLED_PHP_VERSION was set" test -n "${EE_INSTALLED_PHP_VERSION:-}"
check "php reports a runnable version" php -v

# The three extensions EasyEngine requires.
for ext in curl sqlite3 zip; do
check "php extension loaded: $ext" bash -c "php -m | grep -qiE '^${ext}\$'"
done

# Base host dependency installed earlier in check_dependencies().
if ! command -v sqlite3 >/dev/null 2>&1; then
apt-get install -y sqlite3 >/dev/null 2>&1 || true
fi
check "sqlite3 CLI is installed" command -v sqlite3

echo
if [ "$fails" -ne 0 ]; then
echo "RESULT: $fails check(s) failed on ${EE_LINUX_DISTRO}"
exit 1
fi
echo "RESULT: all host-prep checks passed on ${EE_LINUX_DISTRO} (PHP ${EE_INSTALLED_PHP_VERSION:-?})"
40 changes: 40 additions & 0 deletions tests/integration/run-matrix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
#
# Local convenience: run the host-prep integration test across a matrix of
# distro containers, the same way CI does. Requires Docker.
#
# tests/integration/run-matrix.sh # default image set
# tests/integration/run-matrix.sh ubuntu:24.04 # one or more specific images

set -uo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

IMAGES=("$@")
if [ "${#IMAGES[@]}" -eq 0 ]; then
IMAGES=(
ubuntu:22.04
ubuntu:24.04
ubuntu:rolling
ubuntu:devel
debian:12
debian:13
)
fi

rc=0
for img in "${IMAGES[@]}"; do
echo "=================================================================="
echo "==> $img"
echo "=================================================================="
if docker run --rm -v "$REPO_ROOT":/installer:ro -w /installer "$img" \
bash tests/integration/host-prep.sh; then
echo "==> PASS: $img"
else
echo "==> FAIL: $img"
rc=1
fi
echo
done

exit "$rc"
Loading
Loading