Files
shopdb-flask/scripts/check-gates-ran.sh
cproudlock 534f179ee6 Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built
on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It
was neither current nor curated - just stale. This brings it forward and, at the
same time, makes it mean something: main is what a receiving site installs, and
nothing else.

WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides,
knowledgebase, machines, network, notifications, printers, slides, tools,
warranty - plus applications, which is manifest-less core and always ships. The
only manifest dependency inside that set is endpoints on computers, and both are
in it, so the closure is self-contained.

WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb
stay on the site line. So do the Windows installer tree, the VM rehearsal
helpers, the agent tooling config, one site's data loader, the internal audit
and findings records, and the planner spreadsheet. 1,135 tracked files against
1,438.

A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped:

deploy/site-profile-universal.json still named all seventeen plugins. It is the
file docs/DEPLOY.md tells a site to provision from, so a fresh install would
have failed at `flask plugin apply-profile` with PluginError - the first thing a
receiving site does, broken by the branch meant to serve them. It now names the
twelve that exist, and its comment says the two must agree.

agent/ was never refreshed. Whoever populated this branch copied shopdb/,
plugins/, frontend/ and tests/ from the site line and left the agent tree at its
September state: 22 C# and PowerShell sources missing, including AgentCredentials,
EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the
site line had deleted still present. The old tree is internally consistent, so it
compiles, and nothing in CI builds it - every gate was green over a server that
speaks enrollment and per-PC credentials to a bundled agent source that cannot.
Resynced whole.

DECISIONS THIS ENCODES, rather than defers:

- The plugin-authoring lab builds printedparts, whose manifest depends on
  employees, so it could not stay. The lab remains on the site line and main
  ships no build-along tutorial. That is a real loss for an adopting developer
  and is recorded here rather than discovered later.
- Recognition, Training and Recertification notifications are no longer seeded.
  Each splits one notification into a card per employee and renders a photo,
  which needs a plugin this branch does not bundle. Seeding them would offer an
  author a type that cannot resolve anybody. A site that installs employees gets
  them from that plugin's own seed.
- Windows/IIS is documented as the legacy path throughout, and the README leads
  with Docker, because that is what a receiving site gets.

The API description is regenerated for THIS branch - 347 paths and 483
operations, down from 415 and 573 - so the published contract describes what is
here rather than what the site line serves. docs/CONFIG.md lost the two
measuring-tool label keys the code no longer declares, which two lanes had
decided opposite ways.

Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run
rather than skip, and every index line count is derived from the file it names.
2026-09-19 11:24:40 -04:00

153 lines
6.3 KiB
Bash
Executable File

#!/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/*.md 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_api_inventory_parity.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/*.md becomes the wiki, so a published checkout genuinely has nothing to
# check and skipping is correct there. In THIS repo, skipping means something
# broke.
#
# The inventory parity gate used to be here, and that was the hole it exists to
# close. docs/api-inventory.json is not wiki prose: it is a generated artifact,
# tracked exactly like docs/openapi.json beside it, because the app publishes the
# spec built from it. Keying the gate to prose meant a checkout with no docs/*.md
# - which is every fresh clone and every worktree - called itself published and
# ran the route check nowhere. That is where 24 undocumented routes came in.
GATES_INTERNAL_ONLY=(
"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.
#
# NOTE: docs/wiki-drafts/*.md does NOT count here, and that is a standing
# question rather than an oversight - see the comment on the skipif in
# tests/test_docs_publishable.py. Keep the two definitions identical.
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
# REFUSE TO RUN WITHOUT A REAL INTERPRETER. In a git worktree there is no venv,
# `python` is often not on PATH at all, and the pytest line below ends in
# `|| true` - so the run died 127, the report was a single "command not found"
# line, every gate matched nothing, and the script still printed "All gates ran."
# and exited 0. A gate-checker that passes when it ran nothing is worse than no
# gate-checker, because its exit code is what everything else trusts.
if [ -z "${1:-}" ] && ! "$PYTHON" -c 'import pytest' >/dev/null 2>&1; then
echo "FAIL: '$PYTHON' cannot import pytest, so no gate can run here." >&2
echo " Set PYTHON=/path/to/python (a venv that has pytest) and re-run." >&2
exit 1
fi
report="${1:-}"
if [ -z "$report" ]; then
report="$(mktemp)"
# -rs lists every skip with its reason, which is the thing being checked.
# -rf and -rE add the "FAILED <nodeid>" and "ERROR <nodeid>" lines, which -rs
# alone does NOT emit: this script grepped for them anyway, so on a real
# failure it printed its header and then nothing at all.
"$PYTHON" -m pytest "${GATES[@]}" -q -rsfE > "$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
# Name what failed, from whatever the report actually contains. This script can
# be handed a report somebody else produced, and that one may have been run
# without -rf, so the "FAILED <nodeid>" lines are not guaranteed to be there.
# The FAILURES section underlines are: pytest prints "____ test_name ____" above
# every traceback whether the short summary was asked for or not.
list_failures() {
if grep -qE '^(FAILED|ERROR) ' "$1"; then
grep -E '^(FAILED|ERROR) ' "$1"
return
fi
if grep -qE '^_{3,} .+ _{3,}$' "$1"; then
sed -n 's/^_\{3,\} \(.*[^_]\) _\{3,\}$/\1/p' "$1"
return
fi
# Nothing named. Hand over the count line rather than an empty list, so the
# reader knows to open the report instead of believing there is nothing here.
grep -E '[0-9]+ (failed|error)' "$1" | tail -1
echo "(no test names in this report; run without an argument to get them)"
}
# The old condition was "^[0-9]+ failed" or the bare word "error" anywhere,
# which matches a test name, a warning or a docstring. Anchor on the shapes
# pytest actually prints: a short-summary line, a count in the tally, or an
# interrupted collection.
if grep -qE '^(FAILED|ERROR) |(^|, )[0-9]+ (failed|error)|^!+ Interrupted' "$report"; then
echo
echo "Gate tests FAILED (not merely skipped):"
list_failures "$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."