diff --git a/CLAUDE.md b/CLAUDE.md index 21e0f69..42c6ac9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ To update a component version: change `BUILD_` and `BUILD__SRCURL` i ``` nginx (reverse proxy, SSL termination, static files) └─> gunicorn (WSGI unix socket) - └─> Django/parkour2 backend (wui.settings.prod) + └─> Django/parkour2 backend (config.settings.prod) frontend/dist/ (Vue3/Vite built by npm run build, served as static by nginx) postgresql (unix socket in $DEVSHM) ``` @@ -107,7 +107,7 @@ sqlite → python → uv → node → postgresql → nginx ### Build -`nginx.build.sh` compiles nginx (`--with-http_ssl_module`, `--with-http_v2_module`, most optional modules disabled, `--with-debug`, all temp/pid/lock paths under `$DEVSHM`). It applies `nginx.chicken.patch` before `./configure` (`patch ... --forward || true`, so it's idempotent — a stale nginx source tree that already has the patch is fine); the patch is a bit of build-identity fun (`--build=bwakbwakbwak`, `nginx.h` version-string hunk) and its hunk context is nginx-version-specific, so bumping `BUILD_nginx` can require re-fitting the patch (done for 1.31.3 in `cc59d00`, 1.31.4 in `b067910`). If `NGINX_SSL_DHPARAM` is set, the build also generates that 2048-bit dhparam file once if missing. +`nginx.build.sh` compiles nginx (`--with-http_ssl_module`, `--with-http_v2_module`, most optional modules disabled, `--with-debug`, all temp/pid/lock paths under `$DEVSHM`). It applies `nginx.chicken.patch` before `./configure` (`patch ... --forward || true`, so it's idempotent — a stale nginx source tree that already has the patch is fine); the patch is a bit of build-identity fun (`--build=bwakbwakbwak`, `nginx.h` version-string hunk) and its hunk context is nginx-version-specific, so **every** `BUILD_nginx` bump requires re-fitting the `nginx.h` hunk's `-` context lines to the new real version. Because of the `|| true`, a stale hunk fails *silently* (1.31.4 shipped reporting `nginx/1.31.4` instead of `chicken/1.32.2`), so dry-run it first: `patch -p1 --dry-run --forward --input=nginx.chicken.patch` in the extracted source. If `NGINX_SSL_DHPARAM` is set, the build also generates that 2048-bit dhparam file once if missing. ### Routing diff --git a/build.profile b/build.profile index 58c3461..11ff848 100644 --- a/build.profile +++ b/build.profile @@ -1,4 +1,4 @@ -BUILD_TAG=26.08.31 +BUILD_TAG=26.10.05 BUILD_parkour2=parkour2-develop @@ -37,23 +37,23 @@ PGDATABASE=parkour2 BUILD_sqlite=sqlite-3.53.4 BUILD_sqlite_SRCURL="https://beehive.molgen.mpg.de/ee54db5a5b04e77d4883ce0c051a1eba/sqlite-autoconf-3530400.tar.gz" -# md5repo.sh "https://www.python.org/ftp/python/3.13.15/Python-3.13.15.tgz" -BUILD_python=Python-3.13.15 -BUILD_python_SRCURL="https://beehive.molgen.mpg.de/7d2f36334007c01c034ad37c965ff535/${BUILD_python}.tgz" +# md5repo.sh "https://www.python.org/ftp/python/3.13.16/Python-3.13.16.tgz" +BUILD_python=Python-3.13.16 +BUILD_python_SRCURL="https://beehive.molgen.mpg.de/1c314524d94004d7e51148a4a819d191/${BUILD_python}.tgz" # https://github.com/astral-sh/uv/releases -# wget "https://github.com/astral-sh/uv/releases/download/0.12.8/uv-x86_64-unknown-linux-gnu.tar.gz" -BUILD_uv=uv-0.12.8 -BUILD_uv_SRCURL="https://beehive.molgen.mpg.de/e84aacc82a3b7e2cd99fbaa0cb796b55/uv-x86_64-unknown-linux-gnu.tar.gz" +# wget "https://github.com/astral-sh/uv/releases/download/0.12.23/uv-x86_64-unknown-linux-gnu.tar.gz" +BUILD_uv=uv-0.12.23 +BUILD_uv_SRCURL="https://beehive.molgen.mpg.de/48de2ba157b7aacb58edda6ff2872898/uv-x86_64-unknown-linux-gnu.tar.gz" -# wget "https://nodejs.org/download/release/v24.20.0/node-v24.20.0.tar.gz" -BUILD_node=node-24.20.0 -BUILD_node_SRCURL="https://beehive.molgen.mpg.de/6d782834cb05d397ec78d98f59ef716e/${BUILD_node/-/-v}.tar.gz" +# wget "https://nodejs.org/download/release/v24.21.0/node-v24.21.0.tar.gz" +BUILD_node=node-24.21.0 +BUILD_node_SRCURL="https://beehive.molgen.mpg.de/a0d4131e52d9055f8428439542018d44/${BUILD_node/-/-v}.tar.gz" # wget "https://ftp.postgresql.org/pub/source/v16.15/postgresql-16.15.tar.gz" BUILD_postgresql=postgresql-16.15 BUILD_postgresql_SRCURL="https://beehive.molgen.mpg.de/ac0e604676bda7a306f704b916a4e194/${BUILD_postgresql}.tar.gz" -# wget "https://nginx.org/download/nginx-1.31.4.tar.gz" -BUILD_nginx=nginx-1.31.4 -BUILD_nginx_SRCURL="https://beehive.molgen.mpg.de/f73114f560071ae341e57ed123322f4c/${BUILD_nginx}.tar.gz" +# wget "https://nginx.org/download/nginx-1.31.6.tar.gz" +BUILD_nginx=nginx-1.31.6 +BUILD_nginx_SRCURL="https://beehive.molgen.mpg.de/6d59f919382fae46812c44ffbc87498a/${BUILD_nginx}.tar.gz" diff --git a/buildall.sh b/buildall.sh index 1827b90..5282793 100755 --- a/buildall.sh +++ b/buildall.sh @@ -126,7 +126,7 @@ build() { touch "$_tag" else echo "${_pkg}.build.sh failed exit code $r" - exit + exit $r fi fi } @@ -145,3 +145,7 @@ done echo "built target ${PROJECT} done." } 2>&1 | tee "${LOGS}/${logfile}" +# The tee would otherwise mask any failure inside the braces as success. +r=${PIPESTATUS[0]} +[[ $r == 0 ]] || echo "# build FAILED (exit $r), see ${LOGS}/${logfile}" >&2 +exit $r diff --git a/nginx.chicken.patch b/nginx.chicken.patch index 76de93f..e701a77 100644 --- a/nginx.chicken.patch +++ b/nginx.chicken.patch @@ -63,8 +63,8 @@ index f4ccc61..8ff917c 100644 #define _NGINX_H_INCLUDED_ --#define nginx_version 1031003 --#define NGINX_VERSION "1.31.3" +-#define nginx_version 1031006 +-#define NGINX_VERSION "1.31.6" -#define NGINX_VER "nginx/" NGINX_VERSION +#define nginx_version 1032002 +#define NGINX_VERSION "1.32.2" diff --git a/parkour2.build.sh b/parkour2.build.sh index 11e2e0d..6b708c8 100755 --- a/parkour2.build.sh +++ b/parkour2.build.sh @@ -109,7 +109,7 @@ fi cat <<_EOC_ # generated by $0 in $PWD -DJANGO_SETTINGS_MODULE=wui.settings.prod +DJANGO_SETTINGS_MODULE=config.settings.prod SECRET_KEY=${PARKOUR2_SECRET_KEY} diff --git a/profile.build.sh b/profile.build.sh index d41fff0..ba0ceb1 100755 --- a/profile.build.sh +++ b/profile.build.sh @@ -39,7 +39,7 @@ PGDATABASE=${PGDATABASE} DB_USERNAME=${DB_USERNAME} DB_PASSWORD=${DB_PASSWORD} -DJANGO_SETTINGS_MODULE=wui.settings.prod +DJANGO_SETTINGS_MODULE=config.settings.prod DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@/${PGDATABASE}?host=${PGHOST} PARKOUR2_BIND_ADDR=${PARKOUR2_BIND_ADDR} diff --git a/startstop.build.sh b/startstop.build.sh index 2594237..467b86b 100755 --- a/startstop.build.sh +++ b/startstop.build.sh @@ -92,7 +92,7 @@ if not User.objects.filter(email='${PARKOUR2_ADMIN_EMAIL}').exists(): --pid "${DEVSHM}/gunicorn.pid" --access-logfile "${LOGDIR}/gunicorn-access.log" --error-logfile "${LOGDIR}/gunicorn-error.log" - wui.wsgi:application + config.wsgi:application ) gunicorn "\${_opts[@]}" diff --git a/test-parkour2.sh b/test-parkour2.sh new file mode 100755 index 0000000..9e8e322 --- /dev/null +++ b/test-parkour2.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Run this repo's parkour2 regression tests (tests/parkour2//test_*.py) +# against the built parkour2 checkout in ${PROJECT}/${BUILD_parkour2}. +# +# Each test file is copied into the matching backend app for the run and +# removed afterwards, so the checkout stays clean for `git pull`. Postgres +# must be running (`./startstop.sh start postgresql`); Django creates and +# drops its own test_* database, the real one is not touched. +# +# ./test-parkour2.sh # all tests +# ./test-parkour2.sh -v 2 # extra args go to `manage.py test` + +. build.profile + +set -u +set -e + +BACKEND=${PROJECT}/${BUILD_parkour2}/backend +SRC=$(pwd)/tests/parkour2 + +copied=() +cleanup() { rm -f "${copied[@]}"; } +trap cleanup EXIT + +labels=() +for f in "${SRC}"/*/test_*.py; do + rel=${f#${SRC}/} + dst=${BACKEND}/${rel} + if [ -e "${dst}" ]; then + echo "# ${dst} already exists in checkout, refusing to overwrite" >&2 + exit 1 + fi + cp "${f}" "${dst}" + copied+=("${dst}") + rel=${rel%.py} + labels+=("${rel//\//.}") +done + +. ${PROJECT}/profile +cd "${BACKEND}" +python manage.py test --noinput "$@" "${labels[@]}" diff --git a/tests/parkour2/library_sample_shared/test_indexpair_orphan.py b/tests/parkour2/library_sample_shared/test_indexpair_orphan.py new file mode 100644 index 0000000..983c7af --- /dev/null +++ b/tests/parkour2/library_sample_shared/test_indexpair_orphan.py @@ -0,0 +1,52 @@ +"""Regression test: IndexPair whose IndexType was deleted. + +IndexPair.index_type is ForeignKey(on_delete=SET_NULL), so deleting an +IndexType leaves its pairs behind with index_type=None. IndexPair.__str__ +then dereferences self.index_type.is_dual and raises AttributeError, which +makes /admin/library_sample_shared/indexpair/ return HTTP 500 for everyone +(seen on parkour-dev 2026-10-02 after a plate IndexType with pairs was +deleted and re-created). + +Either upstream fix is accepted: a None-safe __str__ (pair survives as an +orphan) or on_delete=CASCADE (pair is deleted along with its type). +""" + +from common.tests import BaseTestCase +from django.urls import reverse + +from .models import IndexI7, IndexPair, IndexType + + +class OrphanedIndexPairTest(BaseTestCase): + def setUp(self): + index_type = IndexType.objects.create( + name="ONT Native Barcoding 96", format="plate" + ) + index1 = IndexI7.objects.create(prefix="NB", number="01", index="ACGT") + index_type.indices_i7.add(index1) + IndexPair.objects.create( + index_type=index_type, index1=index1, char_coord="A", num_coord=1 + ) + + # Same sequence as in the admin: delete the index, then the type. + index1.delete() + index_type.delete() + + self.pair = IndexPair.objects.first() + + def test_str_without_index_type(self): + if self.pair is None: + self.skipTest("pair was cascade-deleted with its IndexType") + self.assertIsNone(self.pair.index_type) + self.assertEqual(str(self.pair), "") + + def test_admin_changelist_with_orphaned_pair(self): + user = self.create_user(email="admin@test.io") + user.is_superuser = True + user.save() + self.client.force_login(user) + + response = self.client.get( + reverse("admin:library_sample_shared_indexpair_changelist") + ) + self.assertEqual(response.status_code, 200)