Skip to content

Commit ad05687

Browse files
committed
add(CI): upgrade between version smoke test
1 parent 4d8f9f3 commit ad05687

7 files changed

Lines changed: 651 additions & 155 deletions

File tree

‎.github/docker/ci/README.md‎

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
# Install checks on MySQL and PostgreSQL
1+
# Install and upgrade checks
22

3-
Installs Evolution CMS onto a real database server and asserts the result, so
4-
the MySQL and PostgreSQL paths of `install/cli-install.php` get the coverage the
5-
sqlite path already gets from the build workflow.
3+
Two scripts, one set of assertions:
64

7-
Run either leg from the repository root:
5+
- `install-and-smoke.sh` installs this tree onto a database and checks what it
6+
put there, then runs the updater over it to catch anything not idempotent.
7+
- `upgrade-and-smoke.sh` installs an **older release**, copies this tree over it
8+
the way unpacking a release archive would, and runs the updater. That is the
9+
only check that replays the migration chain over a schema this branch did not
10+
create - what every existing site does when it moves to a new release.
11+
12+
Both take a `sqlite`, `mysql` or `pgsql` database from `EVO_DB_TYPE`.
13+
14+
## Installing
15+
16+
Run either database leg from the repository root:
817

918
```sh
1019
docker compose -f .github/docker/ci/docker-compose.yml --profile mysql up \
@@ -18,10 +27,43 @@ Clean up with `docker compose -f .github/docker/ci/docker-compose.yml --profile
1827
<mysql|pgsql> down -v`. The container works on a copy of the tree, not a bind
1928
mount, so a run leaves the working tree alone.
2029

30+
sqlite needs no container at all:
31+
32+
```sh
33+
EVO_DB_TYPE=sqlite EVO_APP_DIR="$PWD" .github/docker/ci/install-and-smoke.sh
34+
```
35+
2136
`PHP_VERSION`, `MYSQL_IMAGE_TAG`, `POSTGRES_IMAGE_TAG`, `MYSQL_PORT` and
2237
`POSTGRES_PORT` override the defaults (PHP 8.3, MySQL 9.7 LTS, PostgreSQL 18.6
2338
LTS, the servers' own ports).
2439

40+
## Upgrading
41+
42+
`upgrade-and-smoke.sh` needs two trees: the older one it installs and upgrades
43+
in place, and this one to copy over it. The older tree is left dirty, so give it
44+
a checkout that can be thrown away.
45+
46+
```sh
47+
git clone --depth 1 --branch 3.5.7 https://github.com/evolution-cms/evolution /tmp/evo-3.5.7
48+
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
49+
```
50+
51+
Two things about the source version are worth knowing:
52+
53+
- **No released version installs on sqlite.** `cli-install.php` offered only
54+
`pgsql` and `mysql` through 3.5.7; the sqlite branch is new in this tree. So a
55+
sqlite upgrade leg has to take its source from a ref that carries it - the
56+
`nightly-3.5.x` tag, or any 3.5.x commit after it landed. A tag works here
57+
like any other ref once a release ships sqlite support.
58+
- **No `--skipComposer` before 3.5.8 either.** An older `composerUpdate()` runs
59+
whatever it finds at `core/vendor/bin/composer` and only warns when that is
60+
missing, so the script moves the shim aside: `core/vendor` is committed at
61+
every tag, and a composer update would test the network rather than the CMS.
62+
63+
The source install is checked for hard failures only. A release run on a newer
64+
PHP may emit deprecations that are not this branch's to fix, and they are not
65+
what is under test - the updater's own log is checked strictly.
66+
2567
## What CI runs
2668

2769
The `install` job in `.github/workflows/ci.yml` does not use the image above.
@@ -36,9 +78,18 @@ same checks; the image is what makes a local run reproducible on a machine with
3678
no PHP on it, and what pins the PHP version when a version question is the one
3779
being investigated.
3880

39-
One PHP version, both databases: what the job proves is the installer's MySQL
40-
and PostgreSQL paths, which do not vary with the PHP minor. 8.3 and 8.4 are both
41-
covered by the analysis and unit test jobs.
81+
One PHP version, every database: what the job proves is the installer's database
82+
paths, which do not vary with the PHP minor. 8.3 and 8.4 are both covered by the
83+
analysis and unit test jobs. The sqlite leg starts no container.
84+
85+
The upgrade checks live in their own workflow, `.github/workflows/upgrade.yml`,
86+
off `push` and `pull_request` on purpose: an upgrade regression comes from a
87+
change to the migrations or the seeders, not from every commit, and each leg
88+
costs a full install plus a full update. It runs nightly, and on demand with the
89+
versions to go from and to as inputs (Actions -> Upgrade -> Run workflow):
90+
`from` takes one or more refs of any repository, `to` a ref of this one or blank
91+
for the checked out tree, which is the default - so the unreleased version is
92+
what an upgrade is tested against unless you say otherwise.
4293

4394
## What a run asserts
4495

@@ -50,7 +101,7 @@ covered by the analysis and unit test jobs.
50101
migrations inside an output buffer it later discards, so the image also
51102
routes `error_log` to stderr; a warning raised during a migration cannot be
52103
swallowed.
53-
3. `smoke.php` checks what reached the database, reading the connection the
104+
3. `smoke.php --mode=install` checks what reached the database, reading the connection the
54105
installer just wrote: every table created, the columns the core migrations
55106
add, the seeded document, template, event names, settings, roles and
56107
permissions, the admin account with a hashed password and the Administrator
@@ -60,3 +111,18 @@ covered by the analysis and unit test jobs.
60111
run, which is what catches a migration or seeder that is not idempotent.
61112
5. The site answers over HTTP: the front page renders the seeded document, and
62113
the manager renders its login form.
114+
115+
An upgrade run asserts the same things through `smoke.php --mode=upgrade`, with
116+
the expectations that belong to a fresh install relaxed - the content and the
117+
settings are whichever ones the older site had, not the ones this installer
118+
would have chosen. Two checks replace them, and they are the point of the mode:
119+
120+
- the counts recorded from the older site by `--mode=baseline`, taken before the
121+
copy, must not have **dropped** anywhere. A seeded catalogue may grow, because
122+
topping those up is what the update seeder is for; losing rows is a bug.
123+
- `site_content`, `users` and `user_attributes` must be **unchanged**. Those are
124+
the site, not the release, and an upgrade may not touch them.
125+
126+
`manager_language` and `emailsender` stay exact in both modes: they are answers
127+
the operator gave the installer, and a seeder writing defaults over them would
128+
reset a live site.

‎.github/docker/ci/install-and-smoke.sh‎

Lines changed: 18 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -4,77 +4,37 @@
44
# seeded data present, the updater able to replay the chain over it without
55
# changing anything, and the site answering over HTTP.
66
#
7-
# Runs as the container entrypoint (see Dockerfile); every knob has a default
8-
# so `docker run` with only EVO_DB_HOST set does something sensible.
7+
# Runs as the container entrypoint (see Dockerfile) and, in CI, directly on the
8+
# runner; every knob has a default so `docker run` with only EVO_DB_HOST set
9+
# does something sensible. EVO_DB_TYPE=sqlite needs no server at all.
10+
#
11+
# For the upgrade of an older release to this tree, see upgrade-and-smoke.sh.
912
# pipefail: the installer output goes through tee, and a pipeline that hides
1013
# the exit code of its first command would report a failed install as a pass.
1114
set -euo pipefail
1215

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

15-
EVO_DB_TYPE=${EVO_DB_TYPE:-mysql}
16-
EVO_DB_HOST=${EVO_DB_HOST:-127.0.0.1}
17-
EVO_DB_USER=${EVO_DB_USER:-root}
18-
EVO_DB_PASSWORD=${EVO_DB_PASSWORD:-secret}
19-
EVO_DB_NAME=${EVO_DB_NAME:-evolution}
20-
EVO_DB_PREFIX=${EVO_DB_PREFIX:-evo_}
18+
export EVO_DB_TYPE=${EVO_DB_TYPE:-mysql}
19+
export EVO_DB_HOST=${EVO_DB_HOST:-127.0.0.1}
20+
export EVO_DB_USER=${EVO_DB_USER:-root}
21+
export EVO_DB_PASSWORD=${EVO_DB_PASSWORD:-secret}
22+
export EVO_DB_NAME=${EVO_DB_NAME:-evolution}
23+
export EVO_DB_PREFIX=${EVO_DB_PREFIX:-evo_}
2124

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

31+
# shellcheck source=lib.sh
32+
. "$(dirname "$0")/lib.sh"
33+
2834
LOG=/tmp/install.log
2935
UPDATE_LOG=/tmp/update.log
30-
PORT=${EVO_HTTP_PORT:-8899}
31-
32-
say() {
33-
printf '\n\033[1;36m== %s\033[0m\n' "$1"
34-
}
35-
36-
fail() {
37-
printf '\033[1;31m!! %s\033[0m\n' "$1" >&2
38-
exit 1
39-
}
40-
41-
# Rejects a run that printed anything PHP or PDO would call a problem.
42-
#
43-
# php.ini in the image sends diagnostics to stdout and to stderr (see
44-
# Dockerfile), and both are captured here, so a warning raised inside the output
45-
# buffer the installer later discards still reaches this log.
46-
#
47-
# The patterns are anchored on how PHP formats a diagnostic ("Warning: text in
48-
# /file on line N") rather than on the bare words, which turn up in plenty of
49-
# legitimate output - the settings seeder alone writes rows whose names contain
50-
# "error".
51-
check_log() {
52-
if grep -nE '(^|PHP )(Warning|Notice|Deprecated|Fatal error|Parse error|Recoverable fatal error|Strict Standards):' "$1"; then
53-
fail "the $2 emitted PHP diagnostics"
54-
fi
55-
# SQLSTATE is how both PDO drivers label a failed statement, and the
56-
# installer swallows some of those into plain output instead of exiting.
57-
if grep -nE 'SQLSTATE\[|Uncaught .*Exception|Migration not found|✖' "$1"; then
58-
fail "the $2 reported a database or migration error"
59-
fi
60-
echo "no diagnostics in $(wc -l < "$1") lines of $2 output"
61-
}
6236

63-
say "Waiting for ${EVO_DB_TYPE} at ${EVO_DB_HOST}"
64-
# Compose already gates on a healthcheck, but this script is also meant to be
65-
# runnable on its own against any reachable server.
66-
attempt=0
67-
until php -r '
68-
$dsn = $argv[1] === "pgsql"
69-
? "pgsql:host=" . $argv[2] . ";dbname=" . $argv[5]
70-
: "mysql:host=" . $argv[2];
71-
new PDO($dsn, $argv[3], $argv[4]);
72-
' "$EVO_DB_TYPE" "$EVO_DB_HOST" "$EVO_DB_USER" "$EVO_DB_PASSWORD" "${EVO_DB_BOOTSTRAP_NAME:-$EVO_DB_USER}" 2>/dev/null; do
73-
attempt=$((attempt + 1))
74-
[ "$attempt" -lt 60 ] || fail "database never became reachable"
75-
sleep 2
76-
done
77-
echo "reachable after ${attempt} attempt(s)"
37+
wait_for_db
7838

7939
say "Installing Evolution CMS (${EVO_DB_TYPE}, database '${EVO_DB_NAME}', prefix '${EVO_DB_PREFIX}')"
8040
# --skipComposer=y: core/vendor is committed, so there is nothing to install and
@@ -83,16 +43,12 @@ say "Installing Evolution CMS (${EVO_DB_TYPE}, database '${EVO_DB_NAME}', prefix
8343
# sqlite leg of the build workflow already covers removal.
8444
# Interactive prompts read from stdin; </dev/null makes an unanswered prompt an
8545
# immediate failure instead of a hung job.
46+
mapfile -t db_args < <(installer_db_args)
8647
cd "$APP_DIR/install"
8748
set +e
8849
php cli-install.php \
8950
--typeInstall=1 \
90-
--databaseType="$EVO_DB_TYPE" \
91-
--databaseServer="$EVO_DB_HOST" \
92-
--database="$EVO_DB_NAME" \
93-
--databaseUser="$EVO_DB_USER" \
94-
--databasePassword="$EVO_DB_PASSWORD" \
95-
--tablePrefix="$EVO_DB_PREFIX" \
51+
"${db_args[@]}" \
9652
--cmsAdmin="$EVO_ADMIN" \
9753
--cmsAdminEmail="$EVO_ADMIN_EMAIL" \
9854
--cmsPassword="$EVO_ADMIN_PASSWORD" \
@@ -140,55 +96,6 @@ say "Checking the data survived the update unchanged"
14096
php "$APP_DIR/.github/docker/ci/smoke.php" "$APP_DIR" || fail "the update changed the installed data"
14197

14298
say "Checking the installed site answers over HTTP"
143-
php -S "127.0.0.1:${PORT}" -t "$APP_DIR" > /tmp/server.log 2>&1 &
144-
server=$!
145-
trap 'kill "$server" 2>/dev/null || true' EXIT
146-
147-
php -r '
148-
// The front end and the manager are the two entry points an installed site
149-
// has to serve. The manager answers 404 without an Accept-Language header
150-
// by design, hence the header on the second request.
151-
$port = $argv[1];
152-
$get = function (string $path, array $headers = []) use ($port): array {
153-
$context = stream_context_create(["http" => [
154-
"ignore_errors" => true,
155-
"timeout" => 20,
156-
"header" => $headers,
157-
]]);
158-
$body = @file_get_contents("http://127.0.0.1:{$port}{$path}", false, $context);
159-
$status = isset($http_response_header[0]) ? (int) substr($http_response_header[0], 9, 3) : 0;
160-
return [$status, (string) $body];
161-
};
162-
163-
for ($i = 0; $i < 30; $i++) {
164-
[$status] = $get("/");
165-
if ($status !== 0) {
166-
break;
167-
}
168-
sleep(1);
169-
}
170-
171-
$failures = [];
172-
[$status, $body] = $get("/");
173-
echo "front page: {$status}\n";
174-
$status === 200 or $failures[] = "the front page answered {$status}";
175-
// The seeded document, rendered through the seeded template - proof the
176-
// CMS read its own data rather than merely booting.
177-
str_contains($body, "Install Successful!") or $failures[] = "the front page did not render the seeded document";
178-
179-
[$status, $body] = $get("/manager/index.php", ["Accept-Language: en-US,en;q=0.9"]);
180-
echo "manager: {$status}\n";
181-
$status === 200 or $failures[] = "the manager answered {$status}";
182-
stripos($body, "password") !== false or $failures[] = "the manager did not render its login form";
183-
184-
if ($failures !== []) {
185-
fwrite(STDERR, " - " . implode("\n - ", $failures) . "\n");
186-
exit(1);
187-
}
188-
echo "both entry points served the installed site\n";
189-
' "$PORT" || {
190-
tail -40 /tmp/server.log >&2
191-
fail "the installed site did not answer correctly"
192-
}
99+
http_check "$APP_DIR"
193100

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

0 commit comments

Comments
 (0)