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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ permissions:

jobs:
tests:
# tests/coverage.sh measures six shell files (COVERAGE.md). The threshold
# tests/coverage.sh measures eight shell files (COVERAGE.md). The threshold
# is the lowest of them and is only ever raised (decision 0006). This job
# produces the check "tests / coverage", the required status on master. The
# job id is part of that name, so renaming it silently detaches the
Expand Down
51 changes: 45 additions & 6 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ Standard: decisions 0003 (90 percent per repository, 95 for code the project
writes) and 0004 (bats plus kcov for shell; a build and a boot on LXC as the
acceptance test of a recipe, docs/org-plan.md section 1).

## Measured 2026-09-27
## Measured 2026-09-28

| File | Test | Lines | Note |
| --- | --- | --- | --- |
| `overlay/usr/lib/inithooks/lib/wordpress.sh` | `tests/wordpress.bats` (40 tests) | 99.00 percent (99/100) under kcov | every function and every branch |
| `overlay/usr/lib/inithooks/firstboot.d/40wordpress` | `tests/hook.bats` (29 tests) | 97.73 percent (43/44) under kcov | the hook itself, run for real |
| `overlay/usr/local/bin/keel-wp` | `tests/wrappers.bats` (25 tests) | 100 percent (8/8) under kcov | both cache branches, the quoting, the exit code it hands back, `DEBUG`, and the `turnkey-wp` link run for real |
| `overlay/usr/local/sbin/keel-wordpress-update` | `tests/wrappers.bats` (the same 25) | 100 percent (21/21) under kcov | both guards refused and satisfied, each wp-cli call made to fail, the whole ownership boundary, and the two names it must not take from the environment |
| `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (73 tests) | 98.95 percent (282/285) under kcov | parsing, addresses, deadlines, the container marks, every verdict, and the image carrying none of the build time archive files |
| `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (13 tests) | 100 percent (26/26) under kcov | every way it enables and every way it refuses, including a staging keyring left in the image |
| `conf.d/zz-project-packages` | `tests/project-packages.bats` (14 tests) | 100 percent (31/31) under kcov | shared with keel-nodebb, where the pattern is maintained |
Expand All @@ -19,21 +21,58 @@ acceptance test of a recipe, docs/org-plan.md section 1).
| `conf.d/main` | the build | integration only | build time script, 0004 pragmatic limits |
| `tests/boot-test.sh` | itself | integration only | the thin main of the acceptance test: keel and LXC as root |

Total over the six measured shell files: **99.07 percent (535/540)**, 196 bats
tests, none failing. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow
Total over the eight measured shell files: **99.12 percent (564/569)**, 221
bats tests, none failing. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow
sets to **97**, the lowest measured file. It is only ever raised (decision
0006).

$ COVERAGE_THRESHOLD=97 tests/coverage.sh
kcov line coverage (threshold 97 percent):
100.00 21/21 keel-wordpress-update
100.00 8/8 keel-wp
100.00 31/31 zz-project-packages
100.00 26/26 zzz-keel-archive
99.00 99/100 wordpress.sh
97.73 43/44 40wordpress
98.95 282/285 boot-test-lib.sh
99.00 99/100 wordpress.sh
100.00 54/54 keel-archive-check
100.00 29/29 zz-project-packages
100.00 26/26 keel-archive-check

### The two operator commands, and the link beside each

`keel-wp` and `keel-wordpress-update` are the commands an operator types, and
they are written by this overlay, so decision 0003's 95 percent applies to
them. They were at nothing until 2026-09-28 and are now at 100 percent of
their lines with every branch driven: the cache directory both present and
absent, a `chown` that fails, the exit code handed back, `DEBUG`, the root
guard and the is-this-a-WordPress guard each refused and satisfied, and each
of the two wp-cli calls made to fail so the script stops before it touches
ownership.

Two of the tests assert a name the updater must **not** read. It runs as root
and rewrites the owner and mode of everything under its target, so it takes
that target from `KEEL_TEST_WPROOT` and not from `WPROOT`, which an operator
may already be exporting and which `conf.d/main` uses for the same path; and
it takes the web user from `KEEL_TEST_WP_USER` and never from `USER`, which
in root's login environment is `root`. The web user in these tests is the
sentinel `keel-test-web-user`, deliberately not `$(id -un)`: with the expected
value equal to `$USER`, neither assertion could tell the two apart.

The compatibility names `turnkey-wp` and `turnkey-wordpress-update` are
symlinks (decision 0015 of the handbook) and are **run**, not inspected.
`test -L` says a link exists; it does not say the appliance still answers to
the old name. Two of those tests copy the whole overlay with `cp -TdR`, which
is literally what `fab-apply-overlay` executes, do it twice because the
Makefile applies this overlay twice, and then run the copied command. That is
the build's own copy step, so the link is proved to survive it rather than
assumed to.

What makes it survive is worth stating correctly, because a copy step is the
kind of thing that gets written again from this note. `-R` copies a symlink as
a symlink unless `-L` is given: `-P` is already the default under `-R`, and
`-d` only adds `--preserve=links`, which is about **hard** links and does
nothing for this. So the property is the absence of `-L`, not the presence of
`-d`, and a test asserts exactly that: a plain `cp -TR` still yields a working
`turnkey-wp`, and `cp -TLR` turns it into a second regular file.

### The five lines that are not covered, and why

Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ and on top of that:
directories intentionally contain web-writable executable code; install
only updates and extensions you trust.
- WordPress core is root-owned and does not update automatically. Apply a
supervised core update as ``root`` with ``turnkey-wordpress-update``.
supervised core update as ``root`` with ``keel-wordpress-update``.
The command verifies official WordPress core checksums and restores the
appliance ownership boundary after updating.

Expand Down Expand Up @@ -170,7 +170,7 @@ it either: it is unpacked from a release archive pinned by digest in
checksums. WordPress core therefore updates through WordPress's own mechanism,
either from the dashboard or, preferably on this appliance, with::

turnkey-wordpress-update
keel-wordpress-update

which runs ``wp core update``, verifies WordPress's checksums again and puts
the ownership boundary back: core and ``wp-config.php`` root owned, and only
Expand Down
65 changes: 65 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
turnkey-wordpress-19.0 (4) turnkey; urgency=low

* The two operator commands this appliance writes itself now carry the
project's own name: /usr/local/bin/keel-wp and
/usr/local/sbin/keel-wordpress-update are the real files, and
/usr/local/bin/turnkey-wp and /usr/local/sbin/turnkey-wordpress-update
are symlinks to them. Both names keep working, which is the point: an
operator who pasted a command out of TurnKey's documentation, or who
wrote a script against turnkey-wp last year, must not silently lose it.
The general rule is decision 0015 of the handbook (tracker#12).

* The symlinks are committed into the overlay rather than made in
conf.d/main. fab-apply-overlay copies an overlay with "cp -TdR", and what
keeps a symlink a symlink there is -R without -L: -P is already the
default under -R, and -d only adds --preserve=links, which is about hard
links. So the property is the absence of -L, and that is what the tests
assert -- a plain "cp -TR" still gives a working turnkey-wp and "cp -TLR"
turns it into a second regular file. This recipe also applies its overlay
twice, once through COMMON_OVERLAYS and once as the product-local
ROOT_OVERLAY, and a second copy over an existing link still leaves a link.
Every one of these is asserted by running the copied command, not by
looking at it.

* Both links are relative, turnkey-wp -> keel-wp rather than an absolute
path, because an absolute link does not resolve inside fab-chroot and
conf.d/main calls the command at build time.

* Nothing about what either command does has changed. keel-wp still runs
wp-cli through runuser as www-data, never as root, with an explicit
--path, and still owns /var/www/.wp-cli; keel-wordpress-update still
refuses a caller who is not root, updates core, verifies WordPress's own
per file checksums and puts the ownership boundary back. The refusal now
names whichever of the two names was typed.

* keel-wordpress-update reads its WordPress root, its web user and its
wp-cli path from KEEL_TEST_WPROOT, KEEL_TEST_WP_USER and KEEL_TEST_WP_CLI
when those are set, so the tests can drive it against a scratch tree. The
prefix is deliberate rather than decorative: the script runs as root and
rewrites the owner and the mode of every file under its target, so the
name that chooses the target must not be one an operator could already be
exporting for something else, and WPROOT is exactly such a name --
conf.d/main of this repository uses it for this same path. keel-wp gained
no new knob at all; its WP_DIR, WP_USR and WP_CACHE are the ones it
already shipped with.

* keel-wordpress-update now refuses a target that is not a WordPress, by
the same wp-includes/version.php that conf.d/main asserts after
unpacking, before it rewrites anything. It reads its web user from
KEEL_TEST_WP_USER and never from USER: the previous file opened
USER=www-data, a plain assignment that masks the inherited value, and
reading ${USER:-www-data} instead would take root's login environment and
hand every wp-content runtime directory to root, so uploads and plugin
installs would fail from the first supervised update onwards. Both are
asserted.

* That is what let both scripts be measured: they were at no coverage at
all and are now at 100 percent of their lines, every branch driven, in
tests/wrappers.bats (COVERAGE.md).

* conf.d/main, README.rst and tests/v19.sh call the Keel names. tests/v19.sh
also asks the compatibility names for the same answers, because a link
that resolves is not a link that works.

-- Keel Linux maintainers <admin@keellinux.org> Mon, 28 Sep 2026 03:00:00 +0000

turnkey-wordpress-19.0 (3) turnkey; urgency=low

* The build verifies the project's own APT archive instead of reading it
Expand Down
4 changes: 2 additions & 2 deletions conf.d/main
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ sed -i "s|^allow_url_fopen.*|allow_url_fopen = On|" /etc/php/?.?/apache2/php.ini
grep -q '^allow_url_fopen = On$' /etc/php/?.?/apache2/php.ini

# wp-cli, pinned and verified. The first boot hook is the only thing that uses
# it unattended, and a person uses it afterwards through turnkey-wp.
# it unattended, and a person uses it afterwards through keel-wp.
curl --proto '=https' --tlsv1.2 -fsSL \
"https://github.com/wp-cli/wp-cli/releases/download/v${WP_CLI_VERSION}/wp-cli-${WP_CLI_VERSION}.phar" \
-o /usr/local/bin/wp
Expand Down Expand Up @@ -91,7 +91,7 @@ chown -R "$WEB_USER:$WEB_USER" "$WPROOT"
# WordPress's own per file checksums, asked of WordPress. This is the second,
# independent check on the same bytes: the digest above says the archive is the
# one we pinned, this says the files in it are the ones WordPress published.
turnkey-wp core verify-checksums --version="$WP_VERSION"
keel-wp core verify-checksums --version="$WP_VERSION"

# No wp-config.php, and none is written here. Assert it, because the whole
# point of this recipe is that the layer carries no credential.
Expand Down
22 changes: 22 additions & 0 deletions overlay/usr/local/bin/keel-wp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash -e
# wp-cli against this appliance's WordPress, run as the web server's own user
# and never as root, with the path given explicitly so the command works from
# any directory.
#
# The Keel name is the real command; `turnkey-wp` beside it is a symlink to
# this file (decision 0015 of the handbook), so a script written against the
# old name, or a command pasted out of TurnKey's documentation, keeps working.

[[ -z "$DEBUG" ]] || set -x

WP_DIR=${WP_DIR:-/var/www/wordpress}
WP_USR=${WP_USR:-www-data}
WP_CACHE=${WP_CACHE:-/var/www/.wp-cli}

if [[ ! -d "$WP_CACHE" ]]; then
mkdir -p "$WP_CACHE"
fi
chown -R "$WP_USR":"$WP_USR" "$WP_CACHE"

runuser "$WP_USR" -s /bin/bash \
-c "/usr/local/bin/wp --path='$WP_DIR' $(printf '%q ' "$@")"
15 changes: 0 additions & 15 deletions overlay/usr/local/bin/turnkey-wp

This file was deleted.

1 change: 1 addition & 0 deletions overlay/usr/local/bin/turnkey-wp
48 changes: 48 additions & 0 deletions overlay/usr/local/sbin/keel-wordpress-update
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# A supervised WordPress core update: update core, verify WordPress's own per
# file checksums, and put the appliance's ownership boundary back afterwards
# (README.rst). Automatic core updates are off in wp-config.php because an
# unsupervised update does not restore that boundary.
#
# The Keel name is the real command; `turnkey-wordpress-update` beside it is a
# symlink to this file (decision 0015 of the handbook). The refusals below name
# whichever of the two the operator typed.
set -euo pipefail

# These three are constants of the appliance. They are readable from
# KEEL_TEST_* only so tests/wrappers.bats can drive this script against a
# scratch tree. The prefix is not decoration: this runs as root and rewrites
# the owner and the mode of every file under its target, so the name that
# chooses that target must not be one an operator could already be exporting
# for something else. WPROOT is exactly such a name -- conf.d/main of this
# repository uses it for this same path.
WPROOT="${KEEL_TEST_WPROOT:-/var/www/wordpress}"
WP_USER="${KEEL_TEST_WP_USER:-www-data}"
WP_CLI="${KEEL_TEST_WP_CLI:-/usr/local/bin/wp}"
me=${0##*/}

test "$(id -u)" -eq 0 || {
echo "$me must run as root" >&2
exit 1
}

# And the target is a WordPress before anything is rewritten, whatever chose
# the path. conf.d/main asserts this same file after unpacking the release.
test -f "$WPROOT/wp-includes/version.php" || {
echo "$me: $WPROOT is not a WordPress installation" >&2
exit 1
}

"$WP_CLI" --allow-root --path="$WPROOT" core update
"$WP_CLI" --allow-root --path="$WPROOT" core verify-checksums

chown -R root:root "$WPROOT"
find "$WPROOT" -type d -exec chmod 0755 {} +
find "$WPROOT" -type f -exec chmod 0644 {} +
for runtime_dir in uploads cache upgrade plugins themes; do
install -d -o "$WP_USER" -g "$WP_USER" -m 0755 \
"$WPROOT/wp-content/$runtime_dir"
chown -R "$WP_USER:$WP_USER" "$WPROOT/wp-content/$runtime_dir"
done
chown root:"$WP_USER" "$WPROOT/wp-config.php"
chmod 0640 "$WPROOT/wp-config.php"
24 changes: 0 additions & 24 deletions overlay/usr/local/sbin/turnkey-wordpress-update

This file was deleted.

1 change: 1 addition & 0 deletions overlay/usr/local/sbin/turnkey-wordpress-update
10 changes: 10 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ the spec.
as `PATH` stubs that log every call, and a scratch `INITHOOKS_PATH` whose
`lib` is a symlink to the real library, so kcov measures the file the
appliance ships.
- `wrappers.bats`: unit tests of the two operator commands this overlay
writes, `overlay/usr/local/bin/keel-wp` and
`overlay/usr/local/sbin/keel-wordpress-update`, and of the `turnkey-wp` and
`turnkey-wordpress-update` symlinks beside them (decision 0015 of the
handbook). `runuser`, `chown`, `install` and `id` are `PATH` stubs that log
every call, and wp-cli is a stub named by `WP_CLI`, so no test needs root, a
web server or a network. The compatibility names are run rather than
inspected, once through a copy of the overlay made with `cp -TdR`, which is
what `fab-apply-overlay` executes.
- `keel-archive.bats`: unit tests of `conf.d/zzz-keel-archive`, the last conf
script, which enables the project's signed APT archive and refuses to unless
the key is in the image and the build time source is gone.
Expand All @@ -51,6 +60,7 @@ Debian packages `bats` (1.11) and `kcov` (43); no root:

bats tests/wordpress.bats
bats tests/hook.bats
bats tests/wrappers.bats
bats tests/boot-test.bats
bats tests/keel-archive.bats
COVERAGE_THRESHOLD=95 tests/coverage.sh
Expand Down
7 changes: 4 additions & 3 deletions tests/coverage.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/bin/bash
# Line coverage of the shell this project writes, measured with kcov over
# the bats suite (decision 0004). The measured files are the first boot
# library, the first boot hook itself, the logic of the boot test, the build
# time archive checks and the script that enables the signed archive; the bar of
# library, the first boot hook itself, the two operator commands the overlay
# ships, the logic of the boot test, the build time archive checks and the
# script that enables the signed archive; the bar of
# decision 0003 applies to all of them. Exits 1 below the threshold, 2 when a
# tool is missing. tests/boot-test.sh is the thin main that runs keel and LXC
# as root and is exercised by the container run in test-appliance.yml, not
Expand All @@ -24,7 +25,7 @@ done
report="${COVERAGE_DIR:-$(mktemp -d)}"
# The include pattern is the whitelist, so no exclude pattern is needed; an
# exclude of /tests/ would drop tests/lib/boot-test-lib.sh with it.
kcov --include-pattern=/lib/wordpress.sh,/firstboot.d/40wordpress,/tests/lib/boot-test-lib.sh,/bin/keel-archive-check,/conf.d/zz-project-packages,/conf.d/zzz-keel-archive \
kcov --include-pattern=/lib/wordpress.sh,/firstboot.d/40wordpress,/overlay/usr/local/bin/keel-wp,/overlay/usr/local/sbin/keel-wordpress-update,/tests/lib/boot-test-lib.sh,/bin/keel-archive-check,/conf.d/zz-project-packages,/conf.d/zzz-keel-archive \
"$report" bats "$here"

json="$(find "$report" -mindepth 2 -maxdepth 2 -name coverage.json -not -path "*/kcov-merged/*" | head -1)"
Expand Down
Loading
Loading