Files
shopdb-flask/scripts/test-external-plugin.sh
cproudlock 11f3d00a04
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
Installer prerequisites: REQ-D through REQ-G
REQ-D: restore waitress and tzdata to requirements.in. They existed ONLY in the
generated requirements.txt (hand-added in bf9e60e), so the next
`uv pip compile` would have silently removed the WSGI server and the IANA
timezone database from every Windows install.

REQ-E: split production and development requirements. requirements.txt was
installing pytest, pytest-cov, pytest-flask, coverage, iniconfig and pluggy onto
production servers. Verified on a real Windows Server box before this change.
CI, scripts/test-external-plugin.sh and the dev docs now use requirements-dev.txt.

REQ-F: standardise on Python 3.14. The repo declared four different versions
(Dockerfile 3.12, DEPLOY-WINDOWS-IIS 3.12, INSTALL-WINDOWS-IIS 3.13, CI 3.13,
plus README, web.config and PLUGIN-EXTERNAL-REPO). 3.14 is in active bugfix
support until ~Apr 2027 and supported to Oct 2030; 3.13 entered security-only in
Apr 2026. All four compiled dependencies publish win_amd64 wheels for 3.14
(cryptography via an abi3 wheel), verified by building an offline wheelhouse and
installing it on Windows Server 2025.

REQ-G: state MySQL 8.0 as the standard for new installs; 5.7+/5.6 remain
supported on an existing server.

Lockfiles regenerated with uv pip compile. Production deps 44 -> 38.
2026-08-02 14:15:18 -04:00

122 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# test-external-plugin.sh
#
# Verify an out-of-tree shopdb-flask plugin against a pinned framework build.
# Stand up a throwaway copy of the framework, drop the plugin into
# plugins/<name>/ as a symlink (the mechanism ADR-003 documents for sister
# sites), and run the framework contract tests plus the plugin's own tests.
# Nonzero exit means the plugin is not compatible with that framework ref.
#
# Two modes:
#
# CI / remote (default): clone the framework at FRAMEWORK_REF from
# FRAMEWORK_URL, build a fresh venv, pip install requirements. Needs
# network access to git and PyPI.
#
# Local / offline: set LOCAL_FRAMEWORK to a framework checkout on disk.
# The script exports that checkout at HEAD with `git archive` (no network)
# and reuses the checkout's existing venv, so it runs with no internet.
#
# Inputs (env var, or positional):
# PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required.
# FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main.
# FRAMEWORK_URL framework git URL for CI mode.
# Default: https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
# LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline.
#
# Examples:
# # CI: test the plugin in the current repo against a pinned tag
# PLUGIN_DIR=. FRAMEWORK_REF=v0.5.0 scripts/test-external-plugin.sh
#
# # Offline: test a bundled plugin against this checkout, no network
# LOCAL_FRAMEWORK=. PLUGIN_DIR=plugins/warranty scripts/test-external-plugin.sh
#
# Coverage: a symlinked plugin is discovered and loaded by the plugin loader
# (loadability, manifest validity, core_version range) AND scanned by the
# import-surface contract test - the scanner resolves each plugin dir before
# walking, since bare Path.rglob does not descend symlinks. Optionally keep an
# equivalent import-surface assertion in your own tests/ so violations fail
# in your repo's CI too (see docs/PLUGIN-EXTERNAL-REPO.md).
set -eu
PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}"
FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}"
FRAMEWORK_URL="${FRAMEWORK_URL:-https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git}"
LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}"
if [ -z "$PLUGIN_DIR" ]; then
echo "ERROR: PLUGIN_DIR is required (env var or first argument)." >&2
exit 2
fi
if [ ! -f "$PLUGIN_DIR/manifest.json" ]; then
echo "ERROR: $PLUGIN_DIR has no manifest.json - not a plugin directory." >&2
exit 2
fi
# Absolute path so the symlink still resolves after we cd into the temp tree.
PLUGIN_ABS="$(cd "$PLUGIN_DIR" && pwd)"
# Plugin name: prefer the manifest name, fall back to the directory basename.
PLUGIN_NAME="$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_ABS/manifest.json" | head -n1)"
if [ -z "$PLUGIN_NAME" ]; then
PLUGIN_NAME="$(basename "$PLUGIN_ABS")"
fi
WORKDIR="$(mktemp -d)"
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT
FRAMEWORK="$WORKDIR/framework"
mkdir -p "$FRAMEWORK"
if [ -n "$LOCAL_FRAMEWORK" ]; then
echo "==> Local mode: exporting framework from $LOCAL_FRAMEWORK (HEAD)"
LOCAL_ABS="$(cd "$LOCAL_FRAMEWORK" && pwd)"
git -C "$LOCAL_ABS" archive HEAD | tar -x -C "$FRAMEWORK"
# Reuse the checkout's venv so no pip and no network are needed.
if [ -x "$LOCAL_ABS/venv/bin/python" ]; then
PYTHON="$LOCAL_ABS/venv/bin/python"
elif [ -x "$LOCAL_ABS/.venv/bin/python" ]; then
PYTHON="$LOCAL_ABS/.venv/bin/python"
else
echo "ERROR: no venv found under $LOCAL_ABS (looked for venv/ and .venv/)." >&2
exit 2
fi
else
echo "==> CI mode: cloning $FRAMEWORK_URL @ $FRAMEWORK_REF"
git clone --depth 1 --branch "$FRAMEWORK_REF" "$FRAMEWORK_URL" "$FRAMEWORK"
echo "==> Creating venv and installing requirements"
python3 -m venv "$WORKDIR/venv"
PYTHON="$WORKDIR/venv/bin/python"
"$PYTHON" -m pip install --upgrade pip >/dev/null
"$PYTHON" -m pip install -r "$FRAMEWORK/requirements-dev.txt"
fi
echo "==> Linking plugin '$PLUGIN_NAME' into framework plugins/"
rm -rf "$FRAMEWORK/plugins/$PLUGIN_NAME"
ln -s "$PLUGIN_ABS" "$FRAMEWORK/plugins/$PLUGIN_NAME"
# Drop any exported registry so the loader re-discovers and enables the plugin
# (the test conftest seeds a fresh registry from plugins/*/manifest.json).
rm -f "$FRAMEWORK/instance/plugins.json"
RC=0
echo "==> Running framework contract tests"
( cd "$FRAMEWORK" && "$PYTHON" -m pytest tests/test_plugin_contract.py -q ) || RC=1
if [ -d "$PLUGIN_ABS/tests" ]; then
echo "==> Running plugin's own tests"
( cd "$FRAMEWORK" && "$PYTHON" -m pytest "$PLUGIN_ABS/tests" -q ) || RC=1
else
echo "==> Plugin has no tests/ directory - skipping plugin test step"
fi
if [ "$RC" -eq 0 ]; then
echo "==> PASS: plugin '$PLUGIN_NAME' is compatible with framework ref '$FRAMEWORK_REF'"
else
echo "==> FAIL: plugin '$PLUGIN_NAME' - see output above" >&2
fi
exit "$RC"