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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Keeps the build context small for both images in .github/docker. Neither of
# them needs the history, the editor settings or the scratch directory.
.git
.github/docker/**/*.md
.idea
tmp
node_modules
core/database/*.sqlite
45 changes: 45 additions & 0 deletions .github/docker/ci/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Runs the CLI installer against a real database server, so the MySQL and
# PostgreSQL paths of install/cli-install.php get the same coverage the sqlite
# build (.github/workflows/build.yml) already gives the sqlite one.
#
# php:<version>-cli, not one of the Salo runtimes: the checks here are the
# installer, the migrations and the seeded data, and the built in web server is
# enough to prove the installed site answers. Which web server serves it is what
# the Salo images in .github/docker/Dockerfile are for.
ARG PHP_VERSION=8.3
FROM php:${PHP_VERSION}-cli

# libpq/libzip/libicu are the build inputs of the extensions below; the -dev
# packages stay out of the final image only in multi stage builds, and a CI
# image that lives for one workflow run does not earn the extra stage.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libpq-dev libzip-dev libicu-dev libxml2-dev libonig-dev unzip \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql zip intl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Every diagnostic the installer or a migration emits has to reach the log the
# entrypoint greps. display_errors alone is not enough: the installer runs the
# migrations inside an output buffer it later discards, which would take the
# warnings printed during them with it. Logging to stderr as well puts each one
# outside the buffer, where nothing can swallow it.
RUN { \
echo 'error_reporting = E_ALL'; \
echo 'display_errors = On'; \
echo 'display_startup_errors = On'; \
echo 'log_errors = On'; \
echo 'error_log = /dev/stderr'; \
} > /usr/local/etc/php/conf.d/evo-ci.ini

WORKDIR /var/www/html

# The repository ships core/vendor, so there is no composer step here and the
# installer is called with --skipComposer=y. A copy rather than a bind mount:
# the installer rewrites core/config and deletes install/, which would leave a
# developer running this locally with a dirty working tree.
COPY . /var/www/html

RUN chmod +x .github/docker/ci/install-and-smoke.sh

ENTRYPOINT ["/var/www/html/.github/docker/ci/install-and-smoke.sh"]
128 changes: 128 additions & 0 deletions .github/docker/ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Install and upgrade checks

Two scripts, one set of assertions:

- `install-and-smoke.sh` installs this tree onto a database and checks what it
put there, then runs the updater over it to catch anything not idempotent.
- `upgrade-and-smoke.sh` installs an **older release**, copies this tree over it
the way unpacking a release archive would, and runs the updater. That is the
only check that replays the migration chain over a schema this branch did not
create - what every existing site does when it moves to a new release.

Both take a `sqlite`, `mysql` or `pgsql` database from `EVO_DB_TYPE`.

## Installing

Run either database leg from the repository root:

```sh
docker compose -f .github/docker/ci/docker-compose.yml --profile mysql up \
--build --abort-on-container-exit --exit-code-from install-mysql

docker compose -f .github/docker/ci/docker-compose.yml --profile pgsql up \
--build --abort-on-container-exit --exit-code-from install-pgsql
```

Clean up with `docker compose -f .github/docker/ci/docker-compose.yml --profile
<mysql|pgsql> down -v`. The container works on a copy of the tree, not a bind
mount, so a run leaves the working tree alone.

sqlite needs no container at all:

```sh
EVO_DB_TYPE=sqlite EVO_APP_DIR="$PWD" .github/docker/ci/install-and-smoke.sh
```

`PHP_VERSION`, `MYSQL_IMAGE_TAG`, `POSTGRES_IMAGE_TAG`, `MYSQL_PORT` and
`POSTGRES_PORT` override the defaults (PHP 8.3, MySQL 9.7 LTS, PostgreSQL 18.6
LTS, the servers' own ports).

## Upgrading

`upgrade-and-smoke.sh` needs two trees: the older one it installs and upgrades
in place, and this one to copy over it. The older tree is left dirty, so give it
a checkout that can be thrown away.

```sh
git clone --depth 1 --branch 3.5.7 https://github.com/evolution-cms/evolution /tmp/evo-3.5.7
EVO_FROM_DIR=/tmp/evo-3.5.7 EVO_NEW_DIR="$PWD" EVO_DB_TYPE=mysql EVO_DB_HOST=127.0.0.1 EVO_DB_USER=root EVO_DB_PASSWORD=secret .github/docker/ci/upgrade-and-smoke.sh
```

Two things about the source version are worth knowing:

- **No released version installs on sqlite.** `cli-install.php` offered only
`pgsql` and `mysql` through 3.5.7; the sqlite branch is new in this tree. So a
sqlite upgrade leg has to take its source from a ref that carries it - the
`nightly-3.5.x` tag, or any 3.5.x commit after it landed. A tag works here
like any other ref once a release ships sqlite support.
- **No `--skipComposer` before 3.5.8 either.** An older `composerUpdate()` runs
whatever it finds at `core/vendor/bin/composer` and only warns when that is
missing, so the script moves the shim aside: `core/vendor` is committed at
every tag, and a composer update would test the network rather than the CMS.

The source install is checked for hard failures only. A release run on a newer
PHP may emit deprecations that are not this branch's to fix, and they are not
what is under test - the updater's own log is checked strictly.

## What CI runs

The `install` job in `.github/workflows/ci.yml` does not use the image above.
It starts only the database service from this compose file — an official image,
pulled, never built — and runs `install-and-smoke.sh` directly on the runner,
whose PHP `shivammathur/setup-php` already provides with `pdo_mysql` and
`pdo_pgsql` prebuilt. Building the image there would spend two or three minutes
per leg compiling extensions the runner hands over ready-made, on every push.

The script only needs `EVO_DB_*` and a reachable server, so both paths run the
same checks; the image is what makes a local run reproducible on a machine with
no PHP on it, and what pins the PHP version when a version question is the one
being investigated.

One PHP version, every database: what the job proves is the installer's database
paths, which do not vary with the PHP minor. 8.3 and 8.4 are both covered by the
analysis and unit test jobs. The sqlite leg starts no container.

The upgrade checks live in their own workflow, `.github/workflows/upgrade.yml`,
off `push` and `pull_request` on purpose: an upgrade regression comes from a
change to the migrations or the seeders, not from every commit, and each leg
costs a full install plus a full update. It runs nightly, and on demand with the
versions to go from and to as inputs (Actions -> Upgrade -> Run workflow):
`from` takes one or more refs of any repository, `to` a ref of this one or blank
for the checked out tree, which is the default - so the unreleased version is
what an upgrade is tested against unless you say otherwise.

## What a run asserts

`install-and-smoke.sh` drives it:

1. The installer creates the database itself — neither server image pre-creates
the one being installed into — and runs the whole migration and seed chain.
2. Nothing PHP would call a diagnostic was printed. The installer runs the
migrations inside an output buffer it later discards, so the image also
routes `error_log` to stderr; a warning raised during a migration cannot be
swallowed.
3. `smoke.php --mode=install` checks what reached the database, reading the connection the
installer just wrote: every table created, the columns the core migrations
add, the seeded document, template, event names, settings, roles and
permissions, the admin account with a hashed password and the Administrator
role, and the bundled plugins and modules.
4. The updater (`--typeInstall=2`) replays the same chain over the installed
site, and `smoke.php` runs again — its row counts have to match the first
run, which is what catches a migration or seeder that is not idempotent.
5. The site answers over HTTP: the front page renders the seeded document, and
the manager renders its login form.

An upgrade run asserts the same things through `smoke.php --mode=upgrade`, with
the expectations that belong to a fresh install relaxed - the content and the
settings are whichever ones the older site had, not the ones this installer
would have chosen. Two checks replace them, and they are the point of the mode:

- the counts recorded from the older site by `--mode=baseline`, taken before the
copy, must not have **dropped** anywhere. A seeded catalogue may grow, because
topping those up is what the update seeder is for; losing rows is a bug.
- `site_content`, `users` and `user_attributes` must be **unchanged**. Those are
the site, not the release, and an upgrade may not touch them.

`manager_language` and `emailsender` stay exact in both modes: they are answers
the operator gave the installer, and a seeder writing defaults over them would
reset a live site.
88 changes: 88 additions & 0 deletions .github/docker/ci/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Installs Evolution CMS onto a real database server and asserts the result.
#
# docker compose -f .github/docker/ci/docker-compose.yml --profile mysql up \
# --build --abort-on-container-exit --exit-code-from install-mysql
# docker compose -f .github/docker/ci/docker-compose.yml --profile pgsql up \
# --build --abort-on-container-exit --exit-code-from install-pgsql
#
# One profile per database, so a failing leg names the database in its own
# service. Run from the repository root: the build context is the whole tree.
name: evo-ci

services:
mysql:
profiles: [mysql]
# 9.7 is the LTS of the MySQL 9 series.
image: mysql:${MYSQL_IMAGE_TAG:-9.7}
# Published so the installer can also be driven from the host - which is
# what CI does, to avoid building a PHP image for something the runner
# already has. The install-* services below reach it over the network
# compose creates and ignore this.
ports:
- "${MYSQL_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: secret
# No MYSQL_DATABASE on purpose: the installer is meant to create the
# database itself, and that branch of checkConnectToDatabaseWithBase()
# only runs when the database is missing.
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-psecret"]
interval: 3s
timeout: 5s
retries: 30
start_period: 30s

pgsql:
profiles: [pgsql]
image: postgres:${POSTGRES_IMAGE_TAG:-18.6}
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: evo
POSTGRES_PASSWORD: secret
# The database the installer connects to first. The one it installs into
# (EVO_DB_NAME below) is deliberately a different, missing database, so
# the CREATE DATABASE branch of the installer is exercised.
POSTGRES_DB: evo
healthcheck:
test: ["CMD-SHELL", "pg_isready -U evo -d evo"]
interval: 3s
timeout: 5s
retries: 30
start_period: 30s

install-mysql:
profiles: [mysql]
build:
context: ../../..
dockerfile: .github/docker/ci/Dockerfile
args:
PHP_VERSION: ${PHP_VERSION:-8.3}
depends_on:
mysql:
condition: service_healthy
environment:
EVO_DB_TYPE: mysql
EVO_DB_HOST: mysql
EVO_DB_USER: root
EVO_DB_PASSWORD: secret
EVO_DB_NAME: evolution
EVO_DB_PREFIX: evo_

install-pgsql:
profiles: [pgsql]
build:
context: ../../..
dockerfile: .github/docker/ci/Dockerfile
args:
PHP_VERSION: ${PHP_VERSION:-8.3}
depends_on:
pgsql:
condition: service_healthy
environment:
EVO_DB_TYPE: pgsql
EVO_DB_HOST: pgsql
EVO_DB_USER: evo
EVO_DB_PASSWORD: secret
EVO_DB_NAME: evolution
EVO_DB_PREFIX: evo_
101 changes: 101 additions & 0 deletions .github/docker/ci/install-and-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash
# Installs Evolution CMS onto the database named by the EVO_DB_* environment,
# then proves the installation is sound: no diagnostics from the migrations, the
# seeded data present, the updater able to replay the chain over it without
# changing anything, and the site answering over HTTP.
#
# Runs as the container entrypoint (see Dockerfile) and, in CI, directly on the
# runner; every knob has a default so `docker run` with only EVO_DB_HOST set
# does something sensible. EVO_DB_TYPE=sqlite needs no server at all.
#
# For the upgrade of an older release to this tree, see upgrade-and-smoke.sh.
# pipefail: the installer output goes through tee, and a pipeline that hides
# the exit code of its first command would report a failed install as a pass.
set -euo pipefail

APP_DIR=${EVO_APP_DIR:-/var/www/html}

export EVO_DB_TYPE=${EVO_DB_TYPE:-mysql}
export EVO_DB_HOST=${EVO_DB_HOST:-127.0.0.1}
export EVO_DB_USER=${EVO_DB_USER:-root}
export EVO_DB_PASSWORD=${EVO_DB_PASSWORD:-secret}
export EVO_DB_NAME=${EVO_DB_NAME:-evolution}
export EVO_DB_PREFIX=${EVO_DB_PREFIX:-evo_}

# Exported: smoke.php reads these to check what the installer stored.
export EVO_ADMIN=${EVO_ADMIN:-admin}
export EVO_ADMIN_EMAIL=${EVO_ADMIN_EMAIL:-admin@evo.local}
export EVO_ADMIN_PASSWORD=${EVO_ADMIN_PASSWORD:-Passw0rd123}
export EVO_LANGUAGE=${EVO_LANGUAGE:-en}

# shellcheck source=lib.sh
. "$(dirname "$0")/lib.sh"

LOG=/tmp/install.log
UPDATE_LOG=/tmp/update.log

wait_for_db

say "Installing Evolution CMS (${EVO_DB_TYPE}, database '${EVO_DB_NAME}', prefix '${EVO_DB_PREFIX}')"
# --skipComposer=y: core/vendor is committed, so there is nothing to install and
# a composer update here would test the network rather than the CMS.
# --removeInstall=n: keeping install/ lets the run report what it used, and the
# sqlite leg of the build workflow already covers removal.
# Interactive prompts read from stdin; </dev/null makes an unanswered prompt an
# immediate failure instead of a hung job.
mapfile -t db_args < <(installer_db_args)
cd "$APP_DIR/install"
set +e
php cli-install.php \
--typeInstall=1 \
"${db_args[@]}" \
--cmsAdmin="$EVO_ADMIN" \
--cmsAdminEmail="$EVO_ADMIN_EMAIL" \
--cmsPassword="$EVO_ADMIN_PASSWORD" \
--language="$EVO_LANGUAGE" \
--removeInstall=n \
--skipComposer=y \
< /dev/null 2>&1 | tee "$LOG"
status=$?
set -e
[ "$status" -eq 0 ] || fail "the installer exited with ${status}"
cd "$APP_DIR"

say "Checking the installer output for diagnostics"
check_log "$LOG" "installer"
# The banner is the last thing install() prints, and the only progress message
# that survives: everything the installer says while migrating and seeding goes
# into the output buffer index.php opens, which checkRemoveInstall() discards.
# What actually reached the database is checked below instead.
grep -q 'Now you use' "$LOG" || fail "the installer did not run to completion"

say "Checking the seeded data"
php "$APP_DIR/.github/docker/ci/smoke.php" "$APP_DIR" || fail "the seeded data is not what the installer promises"

say "Running the updater over the site just installed"
# The update path replays the same migration chain against a populated database,
# which is the only way to find a migration that is not idempotent - and it is
# what every existing site runs when it moves to a new release.
cd "$APP_DIR/install"
set +e
php cli-install.php --typeInstall=2 --removeInstall=n < /dev/null 2>&1 | tee "$UPDATE_LOG"
status=$?
set -e
[ "$status" -eq 0 ] || fail "the updater exited with ${status}"
cd "$APP_DIR"

# update() prints its own "Evolution CMS updated!" before checkRemoveInstall()
# discards the buffer, so a successful update says nothing at all; its exit code
# above and the checks below are the evidence. Warnings still arrive, because
# php.ini routes them to stderr as well.
check_log "$UPDATE_LOG" "updater"

say "Checking the data survived the update unchanged"
# Same checks, plus the row counts recorded by the run above: a seeder that
# inserts instead of updating doubles its table here.
php "$APP_DIR/.github/docker/ci/smoke.php" "$APP_DIR" || fail "the update changed the installed data"

say "Checking the installed site answers over HTTP"
http_check "$APP_DIR"

say "OK: ${EVO_DB_TYPE} installation is clean, migrated and seeded"
Loading