Prove the gates ran, and ship the spec the app serves
Two faults with one cause: docs/ is excluded from publication because the prose
becomes the wiki, and two things under docs/ are not prose.
shopdb/core/api/docs.py reads docs/openapi.json AT RUNTIME to serve /api/docs.
Excluding it meant every site installed from the published repo shipped the
ReDoc page, loaded it, and got {"error":"openapi.json not generated"}. The spec
now publishes - it is a product file that happens to live under docs/. The prose
still goes to the wiki and nowhere else, and docs/api-inventory.json stays out,
because nothing reads it at runtime.
The second fault is the reason this was worth chasing. Three gate modules skip
themselves when the file they read is absent, which is correct in a published
checkout and silently wrong on GitHub, which runs against exactly that checkout.
The modules skipped, CI reported green, and an outside branch added nine
undocumented routes and passed. A gate that stops running is worse than no gate,
because nobody looks at it again.
scripts/check-gates-ran.sh asserts each gate ran, with two lists: the ones that
must run everywhere, and the ones needing files publication deliberately leaves
behind - required here, allowed to be absent there. A `gates` job runs it in CI.
Proven in both directions: green in each checkout, and red when the file a gate
reads is removed.
Also purge instance/plugins.json from the publication tree. It was never
published - it is untracked there - but an old copy had sat in that tree for
months, protected from --delete by its own exclude, and it made the published
repo fail its own suite locally: 13 plugins enabled where the code expects 16.
A publication tree that cannot pass its own tests cannot be verified before
shipping. That is the exact trap the comment at the top of the script warns
about, which had claimed a second victim.
Also: concurrency on the workflow, so a second push supersedes the first rather
than queueing behind it holding a self-hosted runner.
This commit is contained in:
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -1,9 +1,11 @@
|
||||
# CI for shopdb-flask on GitHub Actions.
|
||||
#
|
||||
# Mirrors the internal CI pipeline. Four jobs on push + pull_request:
|
||||
# Mirrors the internal CI pipeline. Jobs on push + pull_request:
|
||||
# backend - pytest (SQLite via TestingConfig, no DB service needed)
|
||||
# naming - the CONTRIBUTING.md naming/style gate
|
||||
# gates - proves those checks actually RAN rather than skipping
|
||||
# frontend - vitest + Vue build
|
||||
# lean-build - ADR-013: a per-site build carries only its plugins
|
||||
# migrations-mysql - the REAL multi-site deploy path: fresh flask db upgrade
|
||||
# + every plugin's chain on utf8mb4 MySQL 8, idempotent on
|
||||
# a second run. The pytest suite only exercises SQLite
|
||||
@@ -22,6 +24,12 @@ on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# A second push supersedes the first: the older run answers a question nobody is
|
||||
# asking any more, and it holds a self-hosted runner while doing it.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: arc-runner-set
|
||||
@@ -40,6 +48,24 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- run: bash scripts/check-naming-and-style.sh
|
||||
|
||||
gates:
|
||||
# Prove the gates RAN. Several skip themselves when the file they read is
|
||||
# absent, which is correct in this checkout (docs/ becomes the wiki) and was
|
||||
# silently wrong here for months: the modules skipped, CI went green, and an
|
||||
# outside branch added nine routes documented nowhere and passed.
|
||||
#
|
||||
# A gate that stops running is worse than no gate, because nobody looks at
|
||||
# it again.
|
||||
runs-on: arc-runner-set
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: pip
|
||||
- run: pip install -r requirements-dev.txt
|
||||
- run: bash scripts/check-gates-ran.sh
|
||||
|
||||
frontend:
|
||||
runs-on: arc-runner-set
|
||||
defaults:
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -128,6 +128,25 @@ ADR-007 and ADR-002.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The published site's API documentation page had no spec to show.**
|
||||
`shopdb/core/api/docs.py` reads `docs/openapi.json` at runtime to serve
|
||||
`/api/docs`, and publication excluded the whole of `docs/` because the prose
|
||||
becomes the wiki. So every site installed from the published repo shipped the
|
||||
ReDoc page, loaded it, and answered `{"error":"openapi.json not generated"}`.
|
||||
The spec now publishes: it is a product file that happens to live under
|
||||
`docs/`, not documentation. The prose still goes to the wiki and nowhere else.
|
||||
|
||||
- **Three gates were reporting green by not running.**
|
||||
`test_api_inventory_parity`, `test_openapi_spec` and `test_docs_publishable`
|
||||
all skip themselves when the file they read is absent - correct in a
|
||||
published checkout, and silently wrong on GitHub, which runs against exactly
|
||||
that checkout. An outside branch added nine routes documented nowhere and
|
||||
passed CI. `scripts/check-gates-ran.sh` now asserts each gate actually ran,
|
||||
splitting the ones that must run everywhere from the ones that need files
|
||||
publication deliberately leaves behind, and a new `gates` job runs it in CI.
|
||||
Verified in both directions: it passes here, and it fails when the file a
|
||||
gate reads is taken away.
|
||||
|
||||
- **Secure Send now hands you a message, not a bare URL.** A link on its own
|
||||
invites being pasted onto text already sitting in the chat box, and a chat
|
||||
client turns the selection into the link's caption - so the secret travels as
|
||||
|
||||
101
scripts/check-gates-ran.sh
Executable file
101
scripts/check-gates-ran.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Prove the gates actually RAN.
|
||||
#
|
||||
# Several checks skip themselves when the file they read is absent:
|
||||
#
|
||||
# tests/test_api_inventory_parity.py skipif docs/api-inventory.json missing
|
||||
# tests/test_openapi_spec.py skipif docs/openapi.json missing
|
||||
# tests/test_docs_publishable.py skipif docs/ missing
|
||||
#
|
||||
# That is correct in a published checkout, where docs/ becomes the wiki and
|
||||
# there is genuinely nothing to check. It was silently wrong on GitHub, which
|
||||
# runs against exactly that checkout: the modules skipped, CI reported green,
|
||||
# and an outside branch added nine routes documented nowhere and passed.
|
||||
#
|
||||
# So the list is split. Some gates must run everywhere; the docs-dependent ones
|
||||
# must run HERE, where the files exist, and are allowed to be absent there.
|
||||
#
|
||||
# A gate that stops running is worse than no gate: nobody looks at it again.
|
||||
# This asserts each named module reported at least one PASS and no SKIPS.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/check-gates-ran.sh # runs the gates itself
|
||||
# bash scripts/check-gates-ran.sh report.txt # reads an existing -rs report
|
||||
set -uo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Gates that must run in EVERY checkout, published or internal. A skip here is
|
||||
# always a fault.
|
||||
GATES_EVERYWHERE=(
|
||||
"tests/test_openapi_spec.py"
|
||||
"tests/test_core/test_public_read_surface.py"
|
||||
"tests/test_core/test_authz.py"
|
||||
"tests/test_plugin_migrations.py"
|
||||
)
|
||||
|
||||
# Gates that need files the publication export deliberately leaves behind -
|
||||
# docs/ becomes the wiki, so a published checkout genuinely has nothing to check
|
||||
# and skipping is correct there. In THIS repo, skipping means something broke.
|
||||
GATES_INTERNAL_ONLY=(
|
||||
"tests/test_api_inventory_parity.py"
|
||||
"tests/test_docs_publishable.py"
|
||||
)
|
||||
|
||||
# A published checkout is one with no docs/ prose. openapi.json alone does not
|
||||
# make it internal: that file ships because the app serves it at runtime.
|
||||
if compgen -G "docs/*.md" > /dev/null; then
|
||||
GATES=("${GATES_EVERYWHERE[@]}" "${GATES_INTERNAL_ONLY[@]}")
|
||||
echo "==> Internal checkout: every gate must run"
|
||||
else
|
||||
GATES=("${GATES_EVERYWHERE[@]}")
|
||||
echo "==> Published checkout: docs-dependent gates are expected to be absent"
|
||||
fi
|
||||
|
||||
PYTHON="${PYTHON:-python}"
|
||||
[ -x venv/bin/python ] && PYTHON=venv/bin/python
|
||||
|
||||
report="${1:-}"
|
||||
if [ -z "$report" ]; then
|
||||
report="$(mktemp)"
|
||||
# -rs lists every skip with its reason, which is the thing being checked.
|
||||
"$PYTHON" -m pytest "${GATES[@]}" -q -rs > "$report" 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "==> Checking that every gate ran"
|
||||
|
||||
failed=0
|
||||
for gate in "${GATES[@]}"; do
|
||||
if [ ! -f "$gate" ]; then
|
||||
echo " MISSING $gate - the file is not in this checkout"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
if grep -q "SKIPPED.*$(basename "$gate")" "$report"; then
|
||||
reason=$(grep "SKIPPED.*$(basename "$gate")" "$report" | head -1 | sed 's/.*-- //')
|
||||
echo " SKIPPED $gate"
|
||||
echo " reason: ${reason:-not given}"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
echo " ran $gate"
|
||||
done
|
||||
|
||||
if grep -qE "^[0-9]+ failed|error" "$report"; then
|
||||
echo
|
||||
echo "Gate tests FAILED (not merely skipped):"
|
||||
grep -E "^FAILED|^ERROR" "$report" | sed 's/^/ /'
|
||||
failed=1
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
echo "FAIL: a gate did not run. A skipped gate reports green and checks nothing."
|
||||
echo " If a docs-dependent gate skipped in THIS repo, the file it reads"
|
||||
echo " was not generated: run scripts/gen_openapi.py and"
|
||||
echo " scripts/gen_project_map.py. If one skipped in the PUBLISHED repo,"
|
||||
echo " the export dropped a file the gate needs - see tools/export-github.sh."
|
||||
exit 1
|
||||
fi
|
||||
echo "All gates ran."
|
||||
@@ -41,7 +41,31 @@ done
|
||||
# the publication tree before its exclude existed stays there forever, invisible
|
||||
# to the sync and caught only by the scrub gate. Purge the generated paths first
|
||||
# so adding an exclude is enough on its own.
|
||||
for stale in .pytest_cache .ruff_cache htmlcov .coverage \
|
||||
#
|
||||
# docs/ does NOT publish - the prose becomes the WIKI instead. One exception,
|
||||
# and it is not a documentation decision:
|
||||
#
|
||||
# docs/openapi.json is read AT RUNTIME by shopdb/core/api/docs.py, which
|
||||
# serves the /api/docs page every published site ships. Excluding it meant
|
||||
# that page loaded and then answered {"error":"openapi.json not generated"}
|
||||
# on every site installed from the published repo. It is a product file that
|
||||
# happens to live under docs/, not documentation.
|
||||
#
|
||||
# It also un-skips tests/test_openapi_spec.py, which had been reporting green
|
||||
# by not running - see scripts/check-gates-ran.sh.
|
||||
#
|
||||
# docs/api-inventory.json stays OUT: nothing reads it at runtime, so it is a
|
||||
# development gate and belongs with the wiki. That does mean
|
||||
# test_api_inventory_parity cannot run on GitHub, which is why the gate guard
|
||||
# separates the checks that must run everywhere from the ones that only run
|
||||
# where the repo is complete.
|
||||
# instance/plugins.json is one site's plugin CHOICES. It is excluded, so it was
|
||||
# never published - but an old copy sat in the publication tree for months,
|
||||
# protected from --delete by that same exclude, and it made the published repo's
|
||||
# own test suite fail locally: 13 plugins enabled where the code expects 16.
|
||||
# A publication tree that cannot pass its own tests is not one anybody can
|
||||
# verify before shipping.
|
||||
for stale in .pytest_cache .ruff_cache htmlcov .coverage instance/plugins.json \
|
||||
deploy/windows/installer/bundle deploy/windows/installer/Output \
|
||||
deploy/windows/installer/plugins.iss deploy/windows/installer/version.iss; do
|
||||
rm -rf "${PUB:?}/$stale"
|
||||
@@ -50,7 +74,9 @@ done
|
||||
rsync -a --delete \
|
||||
--exclude '.git' \
|
||||
--exclude '.gitea' \
|
||||
--exclude '/docs' \
|
||||
--include '/docs/' \
|
||||
--include '/docs/openapi.json' \
|
||||
--exclude '/docs/*' \
|
||||
--exclude '/tools' \
|
||||
--exclude '/mcp' \
|
||||
--exclude 'start-api.sh' \
|
||||
|
||||
Reference in New Issue
Block a user