diff --git a/README.rst b/README.rst index ee7badb..dddb202 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Roundup - Issue Tracking System =============================== -`Roundup`_ is a simple-to-use and and powerful issue-tracking system +`Roundup`_ is a simple-to-use and powerful issue-tracking system with command-line, web and e-mail interfaces. Roundup is being used for bug tracking and TODO list management, issue management, customer help desk support, and sales lead tracking. @@ -11,25 +11,24 @@ and on top of that: - Roundup configurations: - - Installed via pip into a python virtual env, within it's own user account. - See /var/www for links to file paths. + - Roundup 2.6 is installed from its verified official PyPI source + distribution in ``/home/roundup/venv``. Python, the MariaDB driver and + timezone data are maintained through Debian Trixie packages. - Roundup served via Apache mod_wsgi. - Domain to serve, set on first boot. - Disabled registration confirmation via email (requires mail server). - - Includes Xapian full text indexer (recommended for large issue DB). - Includes full timezone support and documentation. - **Security note**: Updates to Roundup may require supervision so - they **ARE NOT** configured to install automatically. See `Roundup - documentation`_ for upgrading. Please note specific user account and - location of virtual env when updating. + **Security note**: Roundup updates may require a tracker migration, so + they are not installed automatically. Back up the appliance, read the + `Roundup documentation`_, then install a selected release with:: - As a convenience a script ``roundup-install.sh`` is included. This script - will perform the installation and setup convenience symlinks. All steps - other than installation will still have to be performed manually. + roundup-update VERSION - Usage is: ``roundup-install.sh [opts] `` + The command obtains the release URL and SHA-256 digest from the official + PyPI metadata, updates the deployed virtual environment, runs the Roundup + tracker migration, and restarts Apache. - SSL support out of the box. @@ -55,5 +54,5 @@ Credentials *(passwords set at first boot)* .. _Roundup: https://roundup-tracker.org/ -.. _Roundup documentation: https://roundup.sourceforge.net/docs/upgrading.html +.. _Roundup documentation: https://docs.roundup-tracker.org/en/latest/installation.html#upgrading .. _TurnKey Core: https://www.turnkeylinux.org/core diff --git a/changelog b/changelog index 6320d58..fc5e882 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,20 @@ +turnkey-roundup-19.0 (1) turnkey; urgency=low + + * Upgrade the base distribution to Debian 13/Trixie. + + * Update Roundup to 2.6.0 from its verified official PyPI source + distribution and use Debian packages for its Python runtime, MariaDB + driver and timezone data. + + * Replace the obsolete installer with a supervised updater that verifies + PyPI release metadata and runs the tracker migration. + + * Use Roundup's password API for the firstboot administrator credential. + + * See the TurnKey Core 19.0 changelog for changes common to all appliances. + + -- TurnKey Linux release engineering Mon, 24 Aug 2026 00:00:00 +0000 + turnkey-roundup-18.0 (1) turnkey; urgency=low * Update roundup to 2.3.0. diff --git a/conf.d/main b/conf.d/main index 163429f..49f47e6 100755 --- a/conf.d/main +++ b/conf.d/main @@ -8,28 +8,33 @@ DB_PASS=$(mcookie) RT_TRACKER=/var/lib/roundup/tracker RT_TEMPLATE=classic RT_BACKEND=mysql -RT_VER=$(gh_releases roundup-tracker/roundup | grep -v 'git\|alpha\|beta\|b' | sort -V | tail -1) +RT_VER=2.6.0 +RT_URL=https://files.pythonhosted.org/packages/64/93/17eca3bc4e9bda03c45a9b3bf4fd972d96865efa7b0c09100a55cd58d4e7/roundup-2.6.0.tar.gz +RT_SHA256=12fd8fb806047415f22131f965c6ab4026a28dc63f7054e055c8f891950591d4 RT_CONF=/etc/roundup/tracker-config.ini RT_HOME="/home/$USER" +RT_VENV="$RT_HOME/venv" as_user() { su - "$USER" -c "$*"; } adduser --disabled-login --gecos 'roundup user' --shell /bin/bash $USER -as_user mkdir versions -as_user python3 -m venv "versions/$RT_VER" +as_user python3 -m venv --system-site-packages "$RT_VENV" -as_user echo ". versions/$RT_VER/bin/activate" '>' .bashrc.d/roundup-venv +as_user echo ". $RT_VENV/bin/activate" '>' .bashrc.d/roundup-venv as_user chmod +x .bashrc.d/roundup-venv -as_user python3 -m pip install mysqlclient roundup pytz +as_user "$RT_VENV/bin/python -m pip install --no-deps \ + 'roundup @ $RT_URL#sha256=$RT_SHA256'" +version_output=$(as_user "$RT_VENV/bin/roundup-admin -v") +[[ $version_output == "$RT_VER (python 3.13."*")" ]] # allow Apache access to docs - requires search perms (x on directory) -DOCS="$RT_HOME/versions/$RT_VER/share/doc/roundup/html" +DOCS="$RT_VENV/share/doc/roundup/html" chmod o+x $DOCS{,/_{images,sources,static}} # allow Apache access to venv dir listing -chmod o+x $RT_HOME{,/versions{,/$RT_VER}} +chmod o+x "$RT_HOME" "$RT_VENV" a2enmod wsgi a2enmod rewrite @@ -46,8 +51,8 @@ mkdir -p $RT_TRACKER mkdir /etc/roundup chown -R roundup:roundup $RT_TRACKER OPTIONS="admin_email=admin,dispatcher_email=admin,tracker_web=https://www.example.com/,mail_domain=example.com,mail_host=localhost,rdbms_password=$DB_PASS" -as_user "roundup-admin -i $RT_TRACKER install $RT_TEMPLATE $RT_BACKEND $OPTIONS" -as_user "roundup-admin -i $RT_TRACKER initialise $ADMIN_PASS" +as_user "$RT_VENV/bin/roundup-admin -i $RT_TRACKER install $RT_TEMPLATE $RT_BACKEND $OPTIONS" +as_user "$RT_VENV/bin/roundup-admin -i $RT_TRACKER initialise $ADMIN_PASS" echo "tracker = $RT_TRACKER" >> /etc/roundup/roundup-server.ini chown -R www-data:www-data $RT_TRACKER/db diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..04e6698 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,95 @@ +# Roundup 19.0 testing + +## Scope and source decision + +Debian 13 Trixie does not provide a Roundup package. Roundup 19.0 therefore +uses the official Roundup 2.6.0 source distribution from PyPI. The build pins +its files.pythonhosted.org URL and SHA-256 digest +`12fd8fb806047415f22131f965c6ab4026a28dc63f7054e055c8f891950591d4`. +Roundup 2.6.0 supports Python 3.13 and identifies itself as a stable release. + +Python 3.13, Apache mod_wsgi, MariaDB, the Python MariaDB driver and timezone +data come from signed Debian Trixie repositories. The virtual environment uses +Debian's system Python modules, while pip installs only the pinned Roundup +source distribution with dependency resolution disabled. + +The maintained upstream update path is `roundup-update VERSION`. It reads the +selected release's source URL and SHA-256 digest from official PyPI JSON, +updates the deployed virtual environment with hash verification, runs the +Roundup tracker migration, and restarts Apache. An administrator must review +the selected release and back up the appliance before running it. + +## Acceptance command + +```sh +/sandboxed-git/turnkey/tools/test-v19-appliance roundup \ + --source /home/agent/.local/worktrees/turnkey-apps/roundup/wish-roundup-v19-trixie +``` + +The command builds the Trixie root filesystem, imports it into Docker, boots +the normal systemd and firstboot path, then runs `tests/v19.sh`. + +## README crosswalk + +| README contract | Focused check | Required result | Evidence | +| --- | --- | --- | --- | +| Roundup 2.6 runs in its dedicated Python virtual environment | Query Roundup and Python, import its WSGI handler, and inspect Debian module ownership | Roundup 2.6.0 runs on Python 3.13; WSGI imports; MySQLdb and pytz resolve from Debian | `tests/v19.sh` | +| Apache mod_wsgi serves Roundup over TLS | Validate Apache configuration and module state, then request the tracker | Apache is active; WSGI is loaded; the tracker page is returned over HTTPS | `tests/v19.sh` | +| Firstboot configures the administrator account and domain | Log in through the real CSRF-protected web form with the preseeded administrator password | Roundup creates an authenticated administrator session | `tests/v19.sh` | +| The web issue workflow persists in MariaDB | Create, read and update an issue through the web forms, then read its title from MariaDB | The updated issue appears in both the web response and database | `tests/v19.sh` | +| Local Roundup documentation is available | Request `/docs/` through Apache | The installed Roundup documentation is returned | `tests/v19.sh` | +| Postfix supports local application mail | Check service supervision and the local SMTP listener | Postfix is enabled and active with port 25 bound to loopback | `tests/v19.sh` | +| Webmin manages Apache and MariaDB | Check the modules and request the Webmin HTTPS endpoint | Both modules are installed and Webmin answers on port 12321 | `tests/v19.sh` | +| Debian and upstream components have maintained update paths | Refresh APT metadata, inspect eligible candidates, query official PyPI metadata, and inspect installed direct URL metadata | Trixie candidates remain eligible; installed Debian packages do not change; PyPI exposes a source digest; installed Roundup has the pinned URL and digest | `tests/v19.sh`; `tests/v19-upgrade.sh` | +| Root SSH, backup and other base services are inherited from Core | Cite the unchanged Core layer | Core 19 baseline passes | Core run `20260824t010251z-1634-32241`, source `24c82ee3540ce545422742b0e28ba6b687c53ec2`, verdict `PASS` | + +## Disposable upstream upgrade + +`tests/v19-upgrade.sh` exercises the real compatible-version update path in a +disposable Debian Trixie container. It obtains the official source URLs and +SHA-256 digests from PyPI, installs Roundup 2.5.0, initializes an SQLite-backed +classic tracker, creates and reads an issue, upgrades to 2.6.0, runs +`roundup-admin migrate`, reads the pre-upgrade issue, and updates it. + +```sh +docker run --rm --name tkl-roundup-v19-upgrade \ + --mount type=bind,src=/home/agent/.local/worktrees/turnkey-apps/roundup/wish-roundup-v19-trixie,dst=/src,readonly \ + --mount type=bind,src=/usr/local/share/ca-certificates/microsandbox-ca.crt,dst=/usr/local/share/ca-certificates/microsandbox-ca.crt,readonly \ + debian:trixie-slim /src/tests/v19-upgrade.sh +``` + +The fixture returned: + +```text +No migration action required. At schema version 8. +roundup_upgrade=2.5.0_to_2.6.0 issue=1 create_read_update=pass +``` + +## Accepted evidence + +The exact acceptance command passed in run +`20260824t115118z-474-23163` using harness commit +`b6f8b8c2f3e8f00fd5cf36869e645fc08f01f87e`. The harness built clean source +commit `17be462687b772cec9b72e2acb7dc8dd4f516f55`, whose transported archive had +SHA-256 digest +`8dbc31a2f59f3191a620c9023bbbca9f9cdb1be912f5866e1e2c6e084a7a2487`. +Build, import, normal runtime readiness, inithooks and focused runtime tests all +passed. The retained report records Roundup 2.6.0 on Python 3.13.5, Apache +2.4.68 and MariaDB 11.8.6, plus the successful administrator login and issue +create, read and update flow. + +The authoritative report is +`/home/agent/.local/state/turnkey-v19-harness/runs/roundup/20260824t115118z-474-23163/report.txt`. + +## Known limitation + +Docker runtime validation does not exercise the installer, kernel, bootloader +or physical hardware. Roundup adds no appliance-specific behavior at those +boundaries, so the validated Core 19 baseline supplies inherited evidence. + +## Deferred issues + +The focused acceptance creates and updates an issue but does not exercise +inbound email issue creation. The normal local Postfix dependency and Roundup +mail configuration are validated, while end-to-end delivery would require an +external mail system and is disproportionate for this migration. diff --git a/overlay/etc/apache2/sites-available/roundup.conf b/overlay/etc/apache2/sites-available/roundup.conf index 4b77bc3..d2e5eba 100644 --- a/overlay/etc/apache2/sites-available/roundup.conf +++ b/overlay/etc/apache2/sites-available/roundup.conf @@ -1,14 +1,14 @@ ServerName localhost -Alias /docs /home/roundup/versions/2.3.0/share/doc/roundup/html +Alias /docs /home/roundup/venv/share/doc/roundup/html Alias /robots.txt /var/www/static/robots.txt Alias /favicon.ico /var/www/static/favicon.ico AliasMatch ^/@@file/(.*) /var/lib/roundup/tracker/html/$1 WSGIScriptAlias / /var/lib/roundup/tracker/wsgi.py -WSGIPythonHome /home/roundup/versions/2.3.0 -WSGIPythonPath /home/roundup/versions/2.3.0/lib/python3.11/site-packages +WSGIPythonHome /home/roundup/venv +WSGIPythonPath /home/roundup/venv/lib/python3.13/site-packages UseCanonicalName Off @@ -34,7 +34,7 @@ WSGIPythonPath /home/roundup/versions/2.3.0/lib/python3.11/site-packages Options None - + Require all granted AllowOverride None Options None diff --git a/overlay/usr/lib/inithooks/bin/roundup.py b/overlay/usr/lib/inithooks/bin/roundup.py index 7a09032..8de8b1e 100755 --- a/overlay/usr/lib/inithooks/bin/roundup.py +++ b/overlay/usr/lib/inithooks/bin/roundup.py @@ -8,14 +8,11 @@ DEFAULT=www.example.com """ -import os import sys import getopt from libinithooks import inithooks_cache -import hashlib from libinithooks.dialog_wrapper import Dialog -from mysqlconf import MySQL import subprocess def usage(s=None): @@ -78,22 +75,24 @@ def main(): inithooks_cache.write('APP_DOMAIN', domain) - hashpass = "{SHA}" + hashlib.sha1(password.encode('utf8')).hexdigest() - - m = MySQL() - m.execute('UPDATE roundup._user SET _address=%s WHERE _username=\"admin\";', (email,)) - m.execute('UPDATE roundup._user SET _password=%s WHERE _username=\"admin\";', (hashpass,)) + subprocess.run([ + "/home/roundup/venv/bin/roundup-admin", + "-i", "/var/lib/roundup/tracker", + "-u", "admin:turnkey", + "set", "user1", + "password=%s" % password, + "address=%s" % email, + ], check=True) conf = "/etc/roundup/tracker-config.ini" - subprocess.run(["sed", "-i", "s|^web =.*|web = https://%s/|" % domain, conf]) + subprocess.run(["sed", "-i", "s|^web =.*|web = https://%s/|" % domain, conf], check=True) apache_conf = "/etc/apache2/sites-available/roundup.conf" - subprocess.run(["sed", "-i", "\|RewriteRule|s|https://.*|https://%s/\$1 [L,R=301]|" % domain, apache_conf]) - subprocess.run(["sed", "-i", "\|RewriteCond|s|!^.*|!^%s$|" % domain, apache_conf]) + subprocess.run(["sed", "-i", r"\|RewriteRule|s|https://.*|https://%s/\$1 [L,R=301]|" % domain, apache_conf], check=True) + subprocess.run(["sed", "-i", r"\|RewriteCond|s|!^.*|!^%s$|" % domain, apache_conf], check=True) - subprocess.run(['service', 'apache2', 'restart']) + subprocess.run(['service', 'apache2', 'restart'], check=True) if __name__ == "__main__": main() - diff --git a/overlay/usr/local/bin/roundup-install.sh b/overlay/usr/local/bin/roundup-install.sh deleted file mode 100755 index 43d313b..0000000 --- a/overlay/usr/local/bin/roundup-install.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -e - -usage() { - cat <&2 - usage - exit 1 -} - -RU_HOME='/home/roundup' -RU_LOCAL="$RU_HOME/.local" -RU_VERSIONS="$RU_HOME/versions" -PY_V="$(python3 --version | sed "s|^.* \(3\.[0-9]\+\).*|\1|")" - -is_cached() { - ver="$1" - [ -d "$RU_VERSIONS/roundup-$ver" ] || [ -f "$RU_VERSIONS/roundup-$ver.tar.gz" ] -} - -download_ver() { - ver="$1" - if [ ! -d "$RU_VERSIONS" ]; then - su - roundup -c "mkdir '$RU_VERSIONS'" - fi - cd "$RU_VERSIONS" - su - roundup -c "cd '$RU_VERSIONS'; pip download 'roundup==$ver'" - cd - -} - -install_ver() { - ver="$1" - if [ ! -d "$RU_VERSIONS/roundup-$ver" ]; then - su - roundup -c "cd '$RU_VERSIONS'; tar -xvf 'roundup-$ver.tar.gz'" - fi - - su - roundup -c "cd '$RU_VERSIONS/roundup-$ver';\ -python3 setup.py install --prefix='$RU_LOCAL'" - cd - - - LOCAL=/usr/local - TARGETS="/bin/roundup-admin /bin/roundup-gettext /bin/roundup-mailgw /share/roundup" - for target in $TARGETS; do - ln -sf "${RU_LOCAL}${target}" "${LOCAL}${target}" - done - TARGET="lib/python${PY_V}" - ln -sf "${RU_LOCAL}/${TARGET}/site-packages/roundup" \ - "${LOCAL}/${TARGET}/dist-packages/roundup" - ln -sf "${RU_LOCAL}/${TARGET}/site-packages/roundup-${ver}.dist-info" \ - "${LOCAL}/${TARGET}/dist-packages/roundup-${ver}.dist-info" - ln -sf "${RU_LOCAL}/share/doc/roundup" "/var/www/docs" - ln -sf "${RU_LOCAL}/share/man/man1" "/usr/local/share/man/man1" -} - -POSITIONAL=() -DOWNLOAD= -INSTALL=y -while [[ $# -gt 0 ]]; do - case $1 in - -d|--download) - DOWNLOAD=y - shift - ;; - -n|--no-download) - DOWNLOAD=n - shift - ;; - -o|--download-only) - INSTALL= - shift - ;; - -h|--help) - usage - exit - ;; - *) - POSITIONAL+=("$1") - shift - ;; - esac -done - -if [[ "${#POSITIONAL[@]}" -lt 1 ]]; then - fatal "version is required" -elif [[ "${#POSITIONAL[@]}" -gt 1 ]]; then - fatal "unknown extra args provided: ${POSITIONAL[*]:1}" -fi -VER="${POSITIONAL[0]}" - - -if ! is_cached "$VER"; then - if [ "$DOWNLOAD" == 'n' ]; then - fatal "\"$VER\" not stored locally and -n/--no-download set" - else - download_ver "$VER" - fi -elif [ "$DOWNLOAD" == 'y' ]; then - download_ver "$VER" -fi - -if [ -n "$INSTALL" ]; then - install_ver "$VER" -fi diff --git a/overlay/usr/local/sbin/roundup-update b/overlay/usr/local/sbin/roundup-update new file mode 100755 index 0000000..872c86c --- /dev/null +++ b/overlay/usr/local/sbin/roundup-update @@ -0,0 +1,41 @@ +#!/bin/bash +set -Eeuo pipefail + +if [[ $# -ne 1 || ! $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "usage: roundup-update VERSION" >&2 + exit 2 +fi + +version=$1 +venv=/home/roundup/venv +tracker=/var/lib/roundup/tracker + +read -r url sha256 < <(python3 - "$version" <<'PYTHON' +import json +import sys +import urllib.request + +version = sys.argv[1] +with urllib.request.urlopen( + f"https://pypi.org/pypi/roundup/{version}/json") as response: + metadata = json.load(response) + +for artifact in metadata["urls"]: + if artifact["packagetype"] == "sdist": + print(artifact["url"], artifact["digests"]["sha256"]) + break +else: + raise SystemExit(f"Roundup {version} has no source distribution") +PYTHON +) + +su roundup -s /bin/bash -c \ + "'$venv/bin/python' -m pip install --no-deps --upgrade \ + 'roundup @ $url#sha256=$sha256'" +test "$($venv/bin/python -c 'import roundup; print(roundup.__version__)')" = \ + "$version" +su www-data -s /bin/bash -c \ + "'$venv/bin/roundup-admin' -i '$tracker' migrate" +systemctl restart apache2.service + +printf 'Roundup %s installed and tracker migration completed.\n' "$version" diff --git a/plan/main b/plan/main index c734c58..57b19b4 100644 --- a/plan/main +++ b/plan/main @@ -6,9 +6,6 @@ libapache2-mod-wsgi-py3 webmin-apache python3-pip -python3-dev python3-venv - -pkg-config -build-essential -default-libmysqlclient-dev +python3-mysqldb +python3-tz diff --git a/tests/v19-upgrade.sh b/tests/v19-upgrade.sh new file mode 100755 index 0000000..da11df7 --- /dev/null +++ b/tests/v19-upgrade.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -Eeuo pipefail + +old_version=2.5.0 +new_version=2.6.0 +venv=/tmp/roundup-upgrade-venv +tracker=/tmp/roundup-upgrade-tracker + +if [[ $(id -u) -ne 0 ]]; then + echo 'v19-upgrade.sh must run as root in a disposable Trixie container' >&2 + exit 2 +fi + +export DEBIAN_FRONTEND=noninteractive +apt-get update >/dev/null +apt-get install -y --no-install-recommends \ + ca-certificates python3 python3-venv >/dev/null +python3 -m venv "$venv" + +artifact() { + python3 - "$1" <<'PYTHON' +import json +import sys +import urllib.request + +version = sys.argv[1] +with urllib.request.urlopen( + f'https://pypi.org/pypi/roundup/{version}/json') as response: + metadata = json.load(response) +sdist = next(item for item in metadata['urls'] + if item['packagetype'] == 'sdist') +print(sdist['url'], sdist['digests']['sha256']) +PYTHON +} + +read -r old_url old_sha < <(artifact "$old_version") +read -r new_url new_sha < <(artifact "$new_version") +"$venv/bin/python" -m pip install --no-deps \ + "roundup @ $old_url#sha256=$old_sha" >/dev/null +test "$("$venv/bin/python" -c 'import roundup; print(roundup.__version__)')" = \ + "$old_version" + +mkdir "$tracker" +"$venv/bin/roundup-admin" -i "$tracker" install classic sqlite \ + admin_email=admin@example.com,dispatcher_email=admin@example.com,tracker_web=http://localhost/,mail_domain=example.com,mail_host=localhost \ + >/dev/null +"$venv/bin/roundup-admin" -i "$tracker" initialise upgrade-pass +issue=$("$venv/bin/roundup-admin" -i "$tracker" -u admin:upgrade-pass \ + create issue title='survives Roundup upgrade' priority=3 status=1 \ + assignedto=1) +test -n "$issue" +test "$("$venv/bin/roundup-admin" -i "$tracker" -u admin:upgrade-pass \ + get title "issue$issue")" = 'survives Roundup upgrade' + +"$venv/bin/python" -m pip install --no-deps --upgrade \ + "roundup @ $new_url#sha256=$new_sha" >/dev/null +test "$("$venv/bin/python" -c 'import roundup; print(roundup.__version__)')" = \ + "$new_version" +"$venv/bin/roundup-admin" -i "$tracker" migrate +test "$("$venv/bin/roundup-admin" -i "$tracker" -u admin:upgrade-pass \ + get title "issue$issue")" = 'survives Roundup upgrade' +"$venv/bin/roundup-admin" -i "$tracker" -u admin:upgrade-pass \ + set "issue$issue" title='updated after Roundup upgrade' status=5 +test "$("$venv/bin/roundup-admin" -i "$tracker" -u admin:upgrade-pass \ + get title "issue$issue")" = 'updated after Roundup upgrade' + +printf 'roundup_upgrade=%s_to_%s issue=%s create_read_update=pass\n' \ + "$old_version" "$new_version" "$issue" diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..abbd1d3 --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,172 @@ +#!/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} +base=https://localhost +cookie=/tmp/tkl-roundup-cookie.$$ +page=/tmp/tkl-roundup-page.$$ +headers=/tmp/tkl-roundup-headers.$$ +policy=/tmp/tkl-roundup-policy.$$ + +report_error() { + printf 'test_failure line=%s status=%s command=%q\n' \ + "$1" "$2" "$3" >&2 + exit "$2" +} +trap 'report_error "$LINENO" "$?" "$BASH_COMMAND"' ERR + +cleanup() { + rm -f -- "$cookie" "$page" "$headers" "$policy" +} +trap cleanup EXIT + +csrf_token() { + sed -n '/name="@csrf"/{N;s/.*name="@csrf"[^>]*value="\([^"]*\)".*/\1/p;q}' \ + "$1" +} + +systemctl --quiet is-active apache2.service mariadb.service postfix.service \ + multi-user.target +systemctl --quiet is-enabled apache2.service mariadb.service postfix.service +apache2ctl -t +apache2ctl -M 2>/dev/null | grep -F ' wsgi_module ' >/dev/null + +roundup_version=$(/home/roundup/venv/bin/python -c \ + 'import roundup; print(roundup.__version__)') +python_version=$(/home/roundup/venv/bin/python -c \ + 'import platform; print(platform.python_version())') +test "$roundup_version" = 2.6.0 +[[ $python_version == 3.13.* ]] +/home/roundup/venv/bin/python - <<'PYTHON' +import MySQLdb +import pytz +from roundup.cgi.wsgi_handler import RequestDispatcher + +assert MySQLdb.__file__.startswith('/usr/lib/python3/dist-packages/') +assert pytz.__file__.startswith('/usr/lib/python3/dist-packages/') +assert RequestDispatcher +PYTHON +dpkg-query -S /usr/lib/python3/dist-packages/MySQLdb \ + /usr/lib/python3/dist-packages/pytz >/dev/null + +curl --insecure --fail --silent --show-error --location \ + http://localhost/ >"$page" +grep -q 'Roundup issue tracker' "$page" +grep -q 'Roundup docs' "$page" +curl --insecure --fail --silent --show-error \ + "$base/docs/" >"$page" +grep -qi 'Roundup' "$page" + +curl --insecure --fail --silent --show-error \ + -c "$cookie" "$base/" >"$page" +csrf=$(csrf_token "$page") +test -n "$csrf" +curl --insecure --fail --silent --show-error \ + -b "$cookie" -c "$cookie" "$base/" \ + --data-urlencode '__login_name=admin' \ + --data-urlencode "__login_password=$app_password" \ + --data-urlencode '@action=Login' \ + --data-urlencode "@csrf=$csrf" \ + --data-urlencode '__came_from=https://localhost/' >"$page" +grep -q 'Welcome+admin' "$page" +grep -q roundup_session "$cookie" + +curl --insecure --fail --silent --show-error \ + -b "$cookie" "$base/issue?@template=item" >"$page" +csrf=$(csrf_token "$page") +test -n "$csrf" +curl --insecure --silent --show-error \ + -b "$cookie" -c "$cookie" -D "$headers" -o "$page" \ + "$base/issue" \ + -F 'title=TurnKey v19 acceptance issue' \ + -F 'priority=3' -F 'status=1' -F 'assignedto=1' \ + -F '@note=Created through the Roundup web interface' \ + -F '@template=item' -F '@required=title,priority' \ + -F "@csrf=$csrf" -F '@action=new' +grep -q '^HTTP/.* 302' "$headers" +issue=$(sed -n 's|^[Ll]ocation: .*\/issue\([0-9][0-9]*\).*|\1|p' \ + "$headers" | tr -d '\r') +test -n "$issue" + +curl --insecure --fail --silent --show-error \ + -b "$cookie" "$base/issue$issue?@template=item" >"$page" +grep -q 'value="TurnKey v19 acceptance issue"' "$page" +csrf=$(csrf_token "$page") +test -n "$csrf" +curl --insecure --silent --show-error \ + -b "$cookie" -c "$cookie" -D "$headers" -o "$page" \ + "$base/issue$issue" \ + -F 'title=TurnKey v19 acceptance issue updated' \ + -F 'priority=3' -F 'status=5' -F 'assignedto=1' \ + -F '@note=Updated through the Roundup web interface' \ + -F '@template=item' -F '@required=title,priority' \ + -F "@csrf=$csrf" -F '@action=edit' +grep -q '^HTTP/.* 302' "$headers" +curl --insecure --fail --silent --show-error \ + -b "$cookie" "$base/issue$issue?@template=item" >"$page" +grep -q 'value="TurnKey v19 acceptance issue updated"' "$page" +mariadb --batch --skip-column-names --execute \ + "SELECT _title FROM roundup._issue WHERE id=$issue" | + grep -Fxq 'TurnKey v19 acceptance issue updated' + +dpkg-query -W webmin-apache webmin-mysql >/dev/null +curl --insecure --fail --silent --show-error --head \ + https://127.0.0.1:12321/ >/dev/null +ss -ltn | grep -Eq '127\.0\.0\.1:25[[:space:]]' + +read -r pypi_candidate pypi_sdist_sha < <( + python3 - <<'PYTHON' +import json +import urllib.request + +with urllib.request.urlopen('https://pypi.org/pypi/roundup/json') as response: + metadata = json.load(response) +version = metadata['info']['version'] +artifacts = metadata['releases'][version] +sdist = next(item for item in artifacts if item['packagetype'] == 'sdist') +print(version, sdist['digests']['sha256']) +PYTHON +) +test -n "$pypi_candidate" +test -n "$pypi_sdist_sha" +/home/roundup/venv/bin/python - <<'PYTHON' +import importlib.metadata +import json + +distribution = importlib.metadata.distribution('roundup') +direct_url = json.loads(distribution.read_text('direct_url.json')) +assert direct_url['url'].startswith('https://files.pythonhosted.org/') +assert direct_url['archive_info']['hash'] == ( + 'sha256=12fd8fb806047415f22131f965c6ab4026a28dc63f7054e055c8f891950591d4' +) +PYTHON +test -x /usr/local/sbin/roundup-update + +apache_version=$(dpkg-query -W -f='${Version}' apache2) +mariadb_version=$(dpkg-query -W -f='${Version}' mariadb-server) +python_package=$(dpkg-query -W -f='${Version}' python3) +before="$apache_version|$mariadb_version|$python_package" +apt-get update >/dev/null +for package in apache2 mariadb-server python3 python3-mysqldb python3-tz; do + apt-cache policy "$package" >"$policy" + candidate=$(awk '/Candidate:/ {print $2}' "$policy") + test -n "$candidate" + test "$candidate" != '(none)' + grep -Eq 'trixie|deb13' "$policy" +done +after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' python3)" +test "$after" = "$before" +grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d +! grep -Rqi bookworm /etc/apt/sources.list.d + +cat >"$result" <<EOF +package_source=Debian 13 Trixie APT repositories for Python, Apache, mod_wsgi, MariaDB, Postfix and Python database/timezone modules; verified official PyPI source distribution for Roundup +installed_version=roundup $roundup_version; python $python_version ($python_package); apache2 $apache_version; mariadb-server $mariadb_version +runtime_checks=normal init; Apache, MariaDB and Postfix supervision; HTTPS Roundup and local documentation; administrator web login; web issue create, read and update with MariaDB readback; Webmin HTTPS management endpoint +updater_command=apt-get update and apt-cache policy for Debian packages; PyPI JSON candidate query; roundup-update VERSION for a supervised Roundup upgrade +updater_result=signed Debian metadata refreshed with installed packages unchanged; official PyPI candidate $pypi_candidate with sdist SHA-256 $pypi_sdist_sha; installed direct URL hash verified +updater_channel=Debian and TurnKey Trixie APT repositories; official Roundup project releases on PyPI +integrity_evidence=APT accepted signed repository metadata; installed Roundup direct_url records the pinned files.pythonhosted.org sdist and SHA-256; PyPI candidate exposes an sdist digest; no Bookworm source remained +EOF