From 44502467f95dda2360fc2a3c41a22fae2acf2d10 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 25 Aug 2026 20:12:00 +0000 Subject: [PATCH 1/6] Port Web2py appliance to Trixie Install the supported Web2py 3.3.3 release from its exact official tag and submodule commits while retaining the appliance's Apache WSGI and HTTPS administration behavior. Add MariaDB-backed runtime proof, safer firstboot password handling, and a supervised updater constrained to official Web2py 3.x tags. Document the v19 acceptance boundary and add focused tests for firstboot administrator access, the welcome application, database connectivity, source integrity, and update discovery. Verified with shell syntax, Python parsing, shellcheck, and diff hygiene before appliance acceptance. --- Makefile | 6 ++ README.rst | 12 ++- changelog | 14 +++ conf.d/main | 98 ++++++++++++++++++- docs/v19.0-testing.md | 45 +++++++++ overlay/usr/lib/inithooks/bin/web2py.py | 60 ++++++++++++ overlay/usr/local/sbin/web2py-update | 124 ++++++++++++++++++++++++ tests/v19.sh | 122 +++++++++++++++++++++++ 8 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 docs/v19.0-testing.md create mode 100755 overlay/usr/lib/inithooks/bin/web2py.py create mode 100755 overlay/usr/local/sbin/web2py-update create mode 100755 tests/v19.sh diff --git a/Makefile b/Makefile index bffaec5..637f2bb 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,8 @@ include $(FAB_PATH)/common/mk/turnkey/web2py.mk + +# The shared Web2py configuration follows an unpinned latest-release lookup. +# Keep its Trixie packages and overlays, but install the pinned supported source +# from this appliance instead. +COMMON_CONF := $(filter-out web2py,$(COMMON_CONF)) + include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index c56c27c..a9bbf34 100644 --- a/README.rst +++ b/README.rst @@ -12,16 +12,18 @@ and on top of that: - web2py configurations: - - Installed from upstream source code to /var/www/web2py + - Web2py 3 is installed from a pinned official upstream release in + ``/var/www/web2py``. **Security note**: Updates to web2py may require supervision so - they **ARE NOT** configured to install automatically. Using the "upgrade - now" button (within the webUI admin area) is the easiest way. Otherwise, - please see the `web2py documentation`_ for further info on - upgrading. + they **ARE NOT** configured to install automatically. Run + ``web2py-update --check`` to inspect the supported Web2py 3 channel. + Back up the appliance and review the upstream changes before running + ``web2py-update --apply``. - Serve web2py applications with WSGI on Apache. - Force admin console to be served via SSL. + - Include a MariaDB connection for database-driven Web2py applications. - SSL support out of the box. - Postfix MTA (bound to localhost) to allow sending of email (e.g., diff --git a/changelog b/changelog index bc620d1..12bf817 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,17 @@ +turnkey-web2py-19.0 (1) turnkey; urgency=low + + * Install the supported Web2py 3.3.3 release from its pinned official Git + tag and recorded submodule commits. + + * Add a supervised Web2py 3.x update command. + + * Preserve the Apache WSGI, HTTPS administration, and MariaDB application + boundaries on Debian 13 Trixie. + + * See turnkey-core's 19.0 changelog for common platform changes. + + -- TurnKey Linux maintainers Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-web2py-18.0 (1) turnkey; urgency=low * Install web2py from upstream git: v2.27.1. diff --git a/conf.d/main b/conf.d/main index 1454ab3..498678d 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,11 +1,103 @@ -#!/bin/bash -ex +#!/bin/bash -e +set -x + +VERSION=3.3.3 +TAG=v3.3.3 +COMMIT=a729b848ff6b6471a2792cae8156bf087afc2456 +PYDAL_COMMIT=b515c362e83006e79f681ff6491e4a4ab8566acb +ROCKET_COMMIT=4154030489ebab15b96d1f90a6f288cf4f64dd23 +YATL_COMMIT=c6983a51909f76c48f4a40c93eea7f485321cd3f +REMOTE=https://github.com/web2py/web2py.git W2PROOT=/var/www/web2py +DB_NAME=web2py +DB_USER=web2py + +install -d -m 0755 "$W2PROOT" +git -C "$W2PROOT" init +git -C "$W2PROOT" remote add origin "$REMOTE" +git -C "$W2PROOT" fetch --depth 1 origin "refs/tags/$TAG:refs/tags/$TAG" +git -C "$W2PROOT" checkout --detach "$TAG" +test "$(git -C "$W2PROOT" rev-parse HEAD)" = "$COMMIT" +git -C "$W2PROOT" submodule update --init --recursive --depth 1 +test "$(git -C "$W2PROOT/gluon/packages/pydal" rev-parse HEAD)" = \ + "$PYDAL_COMMIT" +test "$(git -C "$W2PROOT/gluon/packages/rocket3" rev-parse HEAD)" = \ + "$ROCKET_COMMIT" +test "$(git -C "$W2PROOT/gluon/packages/yatl" rev-parse HEAD)" = \ + "$YATL_COMMIT" +git -C "$W2PROOT" fsck --no-dangling + +# Expose the official WSGI entry point through the shared Apache overlay. +cp "$W2PROOT/handlers/wsgihandler.py" "$W2PROOT/wsgihandler.py" # set welcome as web2py default application -cat >$W2PROOT/routes.py<"$W2PROOT/routes.py" <<'EOF' #!/usr/bin/env python default_application = 'welcome' EOF -chown www-data:www-data $W2PROOT/routes.py +service mysql start +mysqladmin create "$DB_NAME" + +# Keep the persistent application credentials out of the build trace. +set +x +DB_PASS=$(mcookie) +mysql --batch <"$W2PROOT/.turnkey-db" </usr/local/share/turnkey-web2py/source <&2 + exit 2 +} + +if [[ $# -ne 1 && ! ($# -eq 2 && $1 == --apply && $2 == --dry-run) ]]; then + usage +fi +case $1 in + --check|--apply) ;; + *) usage ;; +esac + +[[ -r $SOURCE ]] || { + echo "Web2py source provenance is unavailable" >&2 + exit 1 +} +# shellcheck disable=SC1090 +. "$SOURCE" +git_safe=(git -c safe.directory="$WEBROOT" -C "$WEBROOT") + +installed_commit=$("${git_safe[@]}" rev-parse HEAD) +installed_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$WEBROOT") +test "$installed_commit" = "$commit" +test "$installed_version" = "$version" +test "$("${git_safe[@]}" remote get-url origin)" = "$REMOTE" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet + +tag_refs=$(git ls-remote --refs "$REMOTE" 'refs/tags/v3.*') +latest_tag=$(awk '$2 ~ /^refs\/tags\/v3\.[0-9]+\.[0-9]+$/ { + sub("refs/tags/", "", $2); print $2 + }' <<<"$tag_refs" | sort -V | tail -n 1) +[[ $latest_tag =~ ^v3\.[0-9]+\.[0-9]+$ ]] +tag_object=$(awk -v ref="refs/tags/$latest_tag" '$2 == ref {print $1}' \ + <<<"$tag_refs") +peeled=$(git ls-remote "$REMOTE" "refs/tags/$latest_tag^{}" | + awk 'NR == 1 {print $1}') +latest_commit=${peeled:-$tag_object} +[[ $latest_commit =~ ^[0-9a-f]{40}$ ]] +latest_version=${latest_tag#v} + +status=update-available +if [[ $installed_version == "$latest_version" && \ + $installed_commit == "$latest_commit" ]]; then + status=up-to-date +elif dpkg --compare-versions "$installed_version" gt "$latest_version"; then + status=newer-than-channel +fi + +if [[ $1 == --check ]]; then + printf 'installed=%s\ninstalled_commit=%s\nlatest=%s\n' \ + "$installed_version" "$installed_commit" "$latest_version" + printf 'candidate=%s\ntag=%s\nchannel=official-web2py-3.x\nstatus=%s\n' \ + "$latest_commit" "$latest_tag" "$status" + exit 0 +fi + +if [[ ${2:-} == --dry-run ]]; then + printf 'mode=apply-dry-run\ntarget=%s\ncandidate=%s\n' \ + "$latest_version" "$latest_commit" + printf 'tag=%s\nverified=official-web2py-tag\nstatus=%s\n' \ + "$latest_tag" "$status" + exit 0 +fi + +test "$(id -u)" -eq 0 +if [[ $status != update-available ]]; then + echo "Web2py $installed_version is $status on the supported 3.x channel." + exit 0 +fi + +systemctl stop apache2.service +trap 'systemctl start apache2.service' EXIT +target_ref="refs/tags/$latest_tag" +runuser -u www-data -- git -C "$WEBROOT" fetch --force --depth 1 \ + origin "$target_ref:$target_ref" +fetched_commit=$("${git_safe[@]}" rev-parse "$target_ref^{commit}") +test "$fetched_commit" = "$latest_commit" +runuser -u www-data -- git -C "$WEBROOT" checkout --detach "$target_ref" +runuser -u www-data -- git -C "$WEBROOT" submodule sync --recursive +runuser -u www-data -- git -C "$WEBROOT" submodule update --init --recursive \ + --depth 1 +test "$("${git_safe[@]}" rev-parse HEAD)" = "$latest_commit" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet +cp "$WEBROOT/handlers/wsgihandler.py" "$WEBROOT/wsgihandler.py" + +updated_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$WEBROOT") +test "$updated_version" = "$latest_version" +source_tmp=$(mktemp "$SOURCE.XXXXXX") +{ + printf 'version=%s\n' "$latest_version" + printf 'tag=%s\n' "$latest_tag" + printf 'commit=%s\n' "$latest_commit" + printf 'pydal_commit=%s\n' \ + "$(git -C "$WEBROOT/gluon/packages/pydal" rev-parse HEAD)" + printf 'rocket_commit=%s\n' \ + "$(git -C "$WEBROOT/gluon/packages/rocket3" rev-parse HEAD)" + printf 'yatl_commit=%s\n' \ + "$(git -C "$WEBROOT/gluon/packages/yatl" rev-parse HEAD)" + printf 'remote=%s\n' "$REMOTE" +} >"$source_tmp" +chmod 0644 "$source_tmp" +mv "$source_tmp" "$SOURCE" + +chown -R www-data:www-data "$WEBROOT" +chown root:www-data "$WEBROOT/.turnkey-db" +chmod 0640 "$WEBROOT/.turnkey-db" +systemctl start apache2.service +trap - EXIT +printf 'updated=%s\ncommit=%s\ntag=%s\n' \ + "$latest_version" "$latest_commit" "$latest_tag" diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..4dd166f --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,122 @@ +#!/bin/bash +set -Eeuo pipefail +umask 077 + +result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} +app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} +db_password=${TKL_TEST_DB_PASS:?TKL_TEST_DB_PASS is required} +webroot=/var/www/web2py +source_file=/usr/local/share/turnkey-web2py/source +cookie=$(mktemp /tmp/web2py-cookie.XXXXXXXX) +page=$(mktemp /tmp/web2py-page.XXXXXXXX) +headers=$(mktemp /tmp/web2py-headers.XXXXXXXX) +update=$(mktemp /tmp/web2py-update.XXXXXXXX) + +cleanup() { + find "$cookie" "$page" "$headers" "$update" -maxdepth 0 -delete +} +trap cleanup EXIT + +for unit in apache2.service mariadb.service cron.service; do + systemctl --quiet is-active "$unit" + systemctl --quiet is-enabled "$unit" +done +apache2ctl configtest +grep -Fxq 'VERSION_CODENAME=trixie' /etc/os-release +grep -Eq '^turnkey-web2py-19\.0' /etc/turnkey_version + +# shellcheck disable=SC1090 +. "$source_file" +test "$version" = 3.3.3 +test "$tag" = v3.3.3 +test "$commit" = a729b848ff6b6471a2792cae8156bf087afc2456 +test "$pydal_commit" = b515c362e83006e79f681ff6491e4a4ab8566acb +test "$rocket_commit" = 4154030489ebab15b96d1f90a6f288cf4f64dd23 +test "$yatl_commit" = c6983a51909f76c48f4a40c93eea7f485321cd3f +git_safe=(git -c safe.directory="$webroot" -C "$webroot") +test "$("${git_safe[@]}" rev-parse HEAD)" = "$commit" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet +test "$(git -C "$webroot/gluon/packages/pydal" rev-parse HEAD)" = \ + "$pydal_commit" +test "$(git -C "$webroot/gluon/packages/rocket3" rev-parse HEAD)" = \ + "$rocket_commit" +test "$(git -C "$webroot/gluon/packages/yatl" rev-parse HEAD)" = \ + "$yatl_commit" +runtime_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$webroot") +test "$runtime_version" = "$version" +test "$(stat -c '%U:%G:%a' "$webroot/parameters_443.py")" = \ + 'www-data:www-data:640' +test "$(stat -c '%U:%G:%a' "$webroot/.turnkey-db")" = \ + 'root:www-data:640' + +curl --fail --silent --show-error http://127.0.0.1/ >"$page" +grep -Fq 'You are successfully running web2py' "$page" +curl --insecure --fail --silent --show-error https://127.0.0.1/ >"$page" +grep -Fq 'You are successfully running web2py' "$page" + +redirect=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}|%{redirect_url}' \ + http://127.0.0.1/admin) +[[ $redirect == 307\|https://*/admin ]] +curl --insecure --fail --silent --show-error --cookie-jar "$cookie" \ + https://127.0.0.1/admin/default/index >"$page" +grep -Fq 'Login to the Administrative Interface' "$page" +curl --insecure --fail --silent --show-error --location \ + --cookie "$cookie" --cookie-jar "$cookie" \ + --data-urlencode "password=$app_password" \ + --data-urlencode 'send=/admin/default/site' \ + https://127.0.0.1/admin/default/index >"$page" +grep -Fq 'Installed applications' "$page" +grep -Fq 'welcome' "$page" + +dal_result=$(runuser -u www-data -- python3 - "$webroot" <<'PY' +import sys + +root = sys.argv[1] +sys.path.insert(0, root) +from gluon import DAL + +with open(f"{root}/.turnkey-db", encoding="utf-8") as stream: + uri = stream.read().strip().split("=", 1)[1] +db = DAL(uri, migrate=False) +rows = db.executesql("SELECT message FROM turnkey_status WHERE id = 1") +db.close() +assert rows == [("Database connectivity verified",)] +print(rows[0][0]) +PY +) +test "$dal_result" = 'Database connectivity verified' +MYSQL_PWD=$db_password mariadb --user=root --batch --skip-column-names \ + web2py --execute='SELECT message FROM turnkey_status WHERE id = 1' | + grep -Fxq 'Database connectivity verified' + +web2py-update --check >"$update" +latest=$(sed -n 's/^latest=//p' "$update") +candidate=$(sed -n 's/^candidate=//p' "$update") +latest_tag=$(sed -n 's/^tag=//p' "$update") +status=$(sed -n 's/^status=//p' "$update") +[[ $latest =~ ^3\.[0-9]+\.[0-9]+$ ]] +[[ $candidate =~ ^[0-9a-f]{40}$ ]] +test "$latest_tag" = "v$latest" +grep -Fxq 'channel=official-web2py-3.x' "$update" +before=$("${git_safe[@]}" rev-parse HEAD) +apply_plan=$(web2py-update --apply --dry-run) +grep -Fq 'mode=apply-dry-run' <<<"$apply_plan" +grep -Fq "target=$latest" <<<"$apply_plan" +grep -Fq "candidate=$candidate" <<<"$apply_plan" +grep -Fq "tag=$latest_tag" <<<"$apply_plan" +grep -Fq 'verified=official-web2py-tag' <<<"$apply_plan" +test "$("${git_safe[@]}" rev-parse HEAD)" = "$before" +"${git_safe[@]}" diff --quiet + +cat >"$result" < Date: Tue, 25 Aug 2026 20:30:42 +0000 Subject: [PATCH 2/6] Accept PyDAL cursor row containers MySQL cursor fetches return a tuple of rows, while the build and acceptance checks required a list. Assert the exact row count and exact scalar value instead so the checks preserve the database-connectivity contract across the driver's native container type. The first exact Wave 2 build reached this DAL query after source and submodule integrity checks passed; it failed only on the prior container-type comparison. --- conf.d/main | 3 ++- tests/v19.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conf.d/main b/conf.d/main index 498678d..4499295 100755 --- a/conf.d/main +++ b/conf.d/main @@ -86,7 +86,8 @@ with open(f"{root}/.turnkey-db", encoding="utf-8") as stream: uri = stream.read().strip().split("=", 1)[1] db = DAL(uri, migrate=False) rows = db.executesql("SELECT message FROM turnkey_status WHERE id = 1") -assert rows == [("Database connectivity verified",)] +assert len(rows) == 1 +assert rows[0][0] == "Database connectivity verified" db.close() PY diff --git a/tests/v19.sh b/tests/v19.sh index 4dd166f..c0035d2 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -83,7 +83,8 @@ with open(f"{root}/.turnkey-db", encoding="utf-8") as stream: db = DAL(uri, migrate=False) rows = db.executesql("SELECT message FROM turnkey_status WHERE id = 1") db.close() -assert rows == [("Database connectivity verified",)] +assert len(rows) == 1 +assert rows[0][0] == "Database connectivity verified" print(rows[0][0]) PY ) From 100d80dc31049a20a631d2e68a43109728429782 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 02:20:40 +0000 Subject: [PATCH 3/6] Trust pinned submodules during acceptance The disposable acceptance runtime inspects the Web2py source as root while the installed framework is correctly owned by www-data. Git therefore rejects the pinned submodule checks as dubious ownership even though the main repository check already carries a per-command trust exception. Scope safe.directory to each read-only submodule revision check. This preserves Git ownership protection globally while allowing acceptance to verify the exact PyDAL, Rocket3, and YATL commits installed by the appliance. --- tests/v19.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/v19.sh b/tests/v19.sh index c0035d2..b115d7e 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -37,11 +37,14 @@ git_safe=(git -c safe.directory="$webroot" -C "$webroot") test "$("${git_safe[@]}" rev-parse HEAD)" = "$commit" "${git_safe[@]}" diff --quiet "${git_safe[@]}" diff --cached --quiet -test "$(git -C "$webroot/gluon/packages/pydal" rev-parse HEAD)" = \ +test "$(git -c safe.directory="$webroot/gluon/packages/pydal" \ + -C "$webroot/gluon/packages/pydal" rev-parse HEAD)" = \ "$pydal_commit" -test "$(git -C "$webroot/gluon/packages/rocket3" rev-parse HEAD)" = \ +test "$(git -c safe.directory="$webroot/gluon/packages/rocket3" \ + -C "$webroot/gluon/packages/rocket3" rev-parse HEAD)" = \ "$rocket_commit" -test "$(git -C "$webroot/gluon/packages/yatl" rev-parse HEAD)" = \ +test "$(git -c safe.directory="$webroot/gluon/packages/yatl" \ + -C "$webroot/gluon/packages/yatl" rev-parse HEAD)" = \ "$yatl_commit" runtime_version=$(python3 -c \ 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ From b4524d8d6cd9dc07ed5bed44fd585b2a1c6c1757 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 02:54:03 +0000 Subject: [PATCH 4/6] Record Web2py v19 acceptance evidence The focused migration now has a retained passing Docker run, so replace the testing placeholder with the exact run, source, harness, runtime, update, and cleanup evidence. Include the lock timeout and Docker storage limit in the documented command so it reproduces the validated Wave 2 invocation. --- docs/v19.0-testing.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 512ef88..8948621 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -28,12 +28,21 @@ come from Debian Trixie. TKLDEV_CONTAINER=tkldev19-wave2 \ TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +TKL_HARNESS_LOCK_TIMEOUT=3600 \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ /sandboxed-git/turnkey/tools/test-v19-appliance web2py \ --source /home/agent/.local/worktrees/turnkey-apps/web2py/wish-web2py-v19-trixie ``` -Exact retained acceptance evidence is added after the committed behavioral -source passes this command. +Retained run `20260826t022049z-4633-14299` passed this command against source +commit `100d80dc31049a20a631d2e68a43109728429782` and harness commit +`54ca2cff6b98a034089d186b249835b58fc467da`. It built the Trixie root, used +the documented `root.patched` overlayfs fallback, completed normal init and +firstboot, and passed the HTTP and HTTPS welcome requests, HTTPS administrator +login, Web2py DAL and direct MariaDB readback, and updater check and dry run. +The installed runtime was Web2py 3.3.3 on Python 3.13.5. The updater reported +3.3.3 at commit `a729b848ff6b6471a2792cae8156bf087afc2456` as up to date, and cleanup +was verified. ## Deferred issues From 0a200200f777813c18b50c2635a10fa5e3a57edf Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 03:17:01 +0000 Subject: [PATCH 5/6] Read Web2py provenance as its owner The updater changed the www-data-owned source tree and then queried its submodules as root. Git rejected those provenance reads as dubious ownership, which could leave updated code paired with stale metadata. Run the three read-only revision queries as www-data, matching the checkout and submodule operations without broadening Git trust. Add a disposable actual-apply fixture that advances local tagged repositories and verifies revisions, clean worktrees, provenance output, and Apache recovery. Verified with Bash syntax checks, shellcheck, diff hygiene, trust inspection, and the disposable apply fixture. --- overlay/usr/local/sbin/web2py-update | 9 +- tests/web2py-update-apply.sh | 144 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) create mode 100755 tests/web2py-update-apply.sh diff --git a/overlay/usr/local/sbin/web2py-update b/overlay/usr/local/sbin/web2py-update index a0c44dd..b14eaf1 100755 --- a/overlay/usr/local/sbin/web2py-update +++ b/overlay/usr/local/sbin/web2py-update @@ -105,11 +105,14 @@ source_tmp=$(mktemp "$SOURCE.XXXXXX") printf 'tag=%s\n' "$latest_tag" printf 'commit=%s\n' "$latest_commit" printf 'pydal_commit=%s\n' \ - "$(git -C "$WEBROOT/gluon/packages/pydal" rev-parse HEAD)" + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/pydal" rev-parse HEAD)" printf 'rocket_commit=%s\n' \ - "$(git -C "$WEBROOT/gluon/packages/rocket3" rev-parse HEAD)" + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/rocket3" rev-parse HEAD)" printf 'yatl_commit=%s\n' \ - "$(git -C "$WEBROOT/gluon/packages/yatl" rev-parse HEAD)" + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/yatl" rev-parse HEAD)" printf 'remote=%s\n' "$REMOTE" } >"$source_tmp" chmod 0644 "$source_tmp" diff --git a/tests/web2py-update-apply.sh b/tests/web2py-update-apply.sh new file mode 100755 index 0000000..57b4264 --- /dev/null +++ b/tests/web2py-update-apply.sh @@ -0,0 +1,144 @@ +#!/bin/bash +set -Eeuo pipefail + +scratch_parent=${WEB2PY_TEST_SCRATCH_ROOT:-/var/lib/turnkey-web2py-test} +install -d -m 0755 "$scratch_parent" +scratch=$(mktemp -d "$scratch_parent/apply.XXXXXXXX") +chmod 0755 "$scratch" + +cleanup() { + find "$scratch" -depth -delete +} +trap cleanup EXIT + +git_config=(-c user.name=fixture -c user.email=fixture@example.invalid) + +make_submodule() { + local name=$1 + local work="$scratch/${name}-work" + local remote="$scratch/${name}.git" + + git init --quiet "$work" + printf 'initial\n' >"$work/revision" + git -C "$work" add revision + git "${git_config[@]}" -C "$work" commit --quiet -m initial + git -C "$work" rev-parse HEAD >"$scratch/${name}-initial" + printf 'updated\n' >"$work/revision" + git -C "$work" add revision + git "${git_config[@]}" -C "$work" commit --quiet -m updated + git -C "$work" rev-parse HEAD >"$scratch/${name}-updated" + git clone --quiet --bare "$work" "$remote" +} + +for module in pydal rocket3 yatl; do + make_submodule "$module" +done + +upstream_work="$scratch/web2py-work" +upstream="$scratch/web2py.git" +git init --quiet "$upstream_work" +install -d "$upstream_work/gluon/packages" "$upstream_work/handlers" +printf 'VERSION = "3.3.3"\n' >"$upstream_work/gluon/version.py" +printf 'fixture\n' >"$upstream_work/handlers/wsgihandler.py" +printf '/.turnkey-db\n/wsgihandler.py\n' >"$upstream_work/.gitignore" +for module in pydal rocket3 yatl; do + git -c protocol.file.allow=always -C "$upstream_work" submodule add \ + --quiet "$scratch/${module}.git" "gluon/packages/$module" + git -C "$upstream_work/gluon/packages/$module" checkout --quiet \ + "$(<"$scratch/${module}-initial")" +done +git -C "$upstream_work" add . +git "${git_config[@]}" -C "$upstream_work" commit --quiet -m initial +git -C "$upstream_work" tag v3.3.3 +initial_commit=$(git -C "$upstream_work" rev-parse HEAD) + +printf 'VERSION = "3.3.4"\n' >"$upstream_work/gluon/version.py" +for module in pydal rocket3 yatl; do + git -C "$upstream_work/gluon/packages/$module" checkout --quiet \ + "$(<"$scratch/${module}-updated")" +done +git -C "$upstream_work" add . +git "${git_config[@]}" -C "$upstream_work" commit --quiet -m updated +git -C "$upstream_work" tag v3.3.4 +updated_commit=$(git -C "$upstream_work" rev-parse HEAD) +git clone --quiet --bare "$upstream_work" "$upstream" + +safe_remote_env=( + GIT_CONFIG_COUNT=4 + GIT_CONFIG_KEY_0=safe.directory + GIT_CONFIG_VALUE_0="$upstream" + GIT_CONFIG_KEY_1=safe.directory + GIT_CONFIG_VALUE_1="$scratch/pydal.git" + GIT_CONFIG_KEY_2=safe.directory + GIT_CONFIG_VALUE_2="$scratch/rocket3.git" + GIT_CONFIG_KEY_3=safe.directory + GIT_CONFIG_VALUE_3="$scratch/yatl.git" +) +webroot="$scratch/installed" +install -d -o www-data -g www-data "$webroot" +runuser -u www-data -- env GIT_ALLOW_PROTOCOL=file "${safe_remote_env[@]}" \ + git clone --quiet --branch v3.3.3 --recurse-submodules \ + "$upstream" "$webroot" +install -o root -g www-data -m 0640 /dev/null "$webroot/.turnkey-db" +source_file="$scratch/source" +cat >"$source_file" <"$shim_dir/systemctl" <>'$service_log' +case "\$1" in + stop) printf 'stopped\n' >'$service_state' ;; + start) printf 'active\n' >'$service_state' ;; + *) exit 2 ;; +esac +EOF +chmod 0755 "$shim_dir/systemctl" +printf 'active\n' >"$service_state" + +updater="$scratch/web2py-update" +sed -e "s|^WEBROOT=.*|WEBROOT=$webroot|" \ + -e "s|^SOURCE=.*|SOURCE=$source_file|" \ + -e "s|^REMOTE=.*|REMOTE=$upstream|" \ + overlay/usr/local/sbin/web2py-update >"$updater" +chmod 0755 "$updater" +env PATH="$shim_dir:$PATH" GIT_ALLOW_PROTOCOL=file "${safe_remote_env[@]}" \ + "$updater" --apply + +# shellcheck disable=SC1090 +. "$source_file" +test "$version" = 3.3.4 +test "$tag" = v3.3.4 +test "$commit" = "$updated_commit" +for module in pydal rocket3 yatl; do + expected=$(<"$scratch/${module}-updated") + case $module in + pydal) recorded=$pydal_commit ;; + rocket3) recorded=$rocket_commit ;; + yatl) recorded=$yatl_commit ;; + esac + test "$recorded" = "$expected" + test "$(runuser -u www-data -- git -C \ + "$webroot/gluon/packages/$module" rev-parse HEAD)" = "$expected" + runuser -u www-data -- git -C "$webroot/gluon/packages/$module" \ + diff --quiet + runuser -u www-data -- git -C "$webroot/gluon/packages/$module" \ + diff --cached --quiet +done +test "$(runuser -u www-data -- git -C "$webroot" rev-parse HEAD)" = \ + "$updated_commit" +runuser -u www-data -- git -C "$webroot" diff --quiet +runuser -u www-data -- git -C "$webroot" diff --cached --quiet +test "$(<"$service_state")" = active +test "$(<"$service_log")" = $'stop apache2.service\nstart apache2.service' From 9e9c1cd315182cf7bdd4b4d1fe2d84899dbe0bc3 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 03:38:20 +0000 Subject: [PATCH 6/6] Bind Web2py docs to passing evidence The acceptance document still named the pre-fix run and source, so it did not identify the review candidate that resolved the updater ownership failure. Record the retained PASS run, source and harness commits, archive and tree hashes, disposable actual-apply proof, and the nonblocking curl argument exposure. This keeps the documented evidence aligned with the artifacts available for independent review. --- docs/v19.0-testing.md | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 8948621..bc8eaae 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -34,15 +34,38 @@ TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ --source /home/agent/.local/worktrees/turnkey-apps/web2py/wish-web2py-v19-trixie ``` -Retained run `20260826t022049z-4633-14299` passed this command against source -commit `100d80dc31049a20a631d2e68a43109728429782` and harness commit -`54ca2cff6b98a034089d186b249835b58fc467da`. It built the Trixie root, used -the documented `root.patched` overlayfs fallback, completed normal init and -firstboot, and passed the HTTP and HTTPS welcome requests, HTTPS administrator -login, Web2py DAL and direct MariaDB readback, and updater check and dry run. -The installed runtime was Web2py 3.3.3 on Python 3.13.5. The updater reported -3.3.3 at commit `a729b848ff6b6471a2792cae8156bf087afc2456` as up to date, and cleanup -was verified. +Retained run `20260826t031723z-720-6453` passed this command against source +commit `0a200200f777813c18b50c2635a10fa5e3a57edf` and harness commit +`54ca2cff6b98a034089d186b249835b58fc467da`. The commit and transport archive +SHA-256 were both +`8ea9b63cca7935411696da3bad2392735808b6ade1710b67f6868185ef7f38c4`. +The input tree SHA-256 was +`ea1dcccd594e69bb4db2ad51386702ca3ffb77d598b77a01f782078c3f09d235`, +and the build tree SHA-256 was +`2bbac7162e4bcde1ba26580d7c22d86c5a786eabd3391455726e97f4805532ba`. +The run built the Trixie root, used the documented `root.patched` overlayfs +fallback, completed normal init and firstboot, and passed the HTTP and HTTPS +welcome requests, HTTPS administrator login, Web2py DAL and direct MariaDB +readback, and updater check and dry run. The installed runtime was Web2py 3.3.3 +on Python 3.13.5. The updater reported 3.3.3 at commit +`a729b848ff6b6471a2792cae8156bf087afc2456` as up to date. Runtime tests and +cleanup passed, and the retained verdict is `PASS` with exit status 0. + +## Disposable updater apply proof + +The updater's real apply and provenance branch was exercised with local tagged +repositories using: + +```sh +sudo env \ + WEB2PY_TEST_SCRATCH_ROOT=/home/agent/.local/state/turnkey-web2py-test \ + ./tests/web2py-update-apply.sh +``` + +The command completed successfully after advancing the fixture from v3.3.3 to +v3.3.4. Its assertions verified the matching top-level and three submodule +revisions, clean source trees, serialized provenance, and Apache service +recovery. ## Deferred issues @@ -51,4 +74,6 @@ was verified. substituting the separate Py4web product. - The updater follows supported 3.x patch tags. Major migration to Py4web changes the product and remains outside this migration. +- **MEDIUM, nonblocking:** the acceptance test's administrator password is + transiently visible in the local `curl` process arguments during login. - Docker acceptance does not repeat installer, kernel, or hardware checks.