Per-PC credentials (replaces the 09-14 client-certificate attempt): - POST /api/endpoints/enroll issues a credential to a PC that matches exactly one computer by hostname and BIOS serial and has none yet. Anything else (unknown or duplicate hostname, serial mismatch, already enrolled) waits as a request an admin approves or rejects. The agent's persisted nonce binds an approved request to the PC that asked. - One auth helper serves every agent route. With X-Endpoint-Credential the asset comes from the credential and a request naming another PC gets 403 ASSET_MISMATCH. The shared key still works for PCs that have not enrolled, is refused (401 SHARED_KEY_REFUSED) for any PC that has, and can be switched off. A revoked credential gets 401 CREDENTIAL_INVALID so the agent re-enrolls. - The core collector accepts the credential for the computers, printers and endpoints collectors through two new plugin hooks exposed on shopdb.api (contract 0.33.0), and refuses the shared key for enrolled PCs. - Admin API: enrollment list, approve, reject, revoke (un-enroll), settings. Migration API: per-PC readiness with blockers, and upgrade waves that package the agent once and queue the existing agent upgrade per PC. - Migration 0023 creates endpointcredentials, endpointenrollrequests and endpointmigrationwaves, drops the certificate columns from assets where test databases have them, and renames app_fingerprint to appfingerprint. 0021 and 0022 keep their ids but no longer alter the core assets table. - Removed: client_cert.py, certificate renewal, the unauthenticated /test/client-cert route, nginx client certificate forwarding and the agent certificate script. The client-cert header could be forged on IIS, the key was readable by users, and the shared key remained a fallback. - GET /settings/server-cert returns the SPKI SHA-256 pin for GE-Enforce. Review fixes: - report_command_result used g without importing it; every result was a 500. - collect_log refuses setup answer files, appsettings, agent directories and key files at enqueue. - Fleet alerts: alerttimeout and alertfleet agree with the migration, the scope is validated and a malformed scope no longer reaches the whole fleet, and the ack race no longer raises NameError. - A refused break-glass override leaves no hold and no audit row. - Agent upgrade checks locks and permissions before repackaging, and writes the package atomically. - Server certificate upload validates the pair before replacing files; generation no longer builds an openssl config from user input; audit rows are actually written. - Health evaluation runs in a savepoint so a caught error cannot lose the check-in. - Required-app policies are found by appid, survive renames, and deleting an app deactivates policies that still name it. The UI and API now say such a policy only takes effect inside a business policy. - Backfill handles duplicate fingerprints and rolls back a failed batch. - Plugin settings routes skip a plugin whose hook raises. - The setup token is no longer printed to the log. - The naming gate checks top-level files again and handles odd file names. - Removed root scratch scripts, one of which carried a token. - Rewrote the fleet alert scheduling and app delta tests on real fixtures.
474 lines
23 KiB
Bash
Executable File
474 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Pre-commit naming + style check for shopdb-flask.
|
|
# Enforces CONTRIBUTING.md rules:
|
|
# 1. No non-ASCII chars in source (em-dashes, smart quotes, arrows, emojis)
|
|
# 2. No banned shorthand identifiers (cfg, ctx, mgr, req, res, env, util, helper)
|
|
# as standalone names (suffix usage like printers_bp, request_obj is allowed)
|
|
# 3. No snake_case DB column names in __tablename__ or db.Column attrs
|
|
# 4. No snake_case API params in frontend that should match DB column names
|
|
#
|
|
# Exits non-zero if any violation found.
|
|
# Skips: venv/, node_modules/, __pycache__/, frontend/dist/, migrations/versions/,
|
|
# deploy/windows/installer/bundle/ (installer build output)
|
|
|
|
set -e
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
|
cd "$REPO_ROOT"
|
|
|
|
VIOLATIONS=0
|
|
|
|
# Use git ls-files for 5x+ speedup - only scans tracked files, respects .gitignore automatically
|
|
# Exclude generated/vendor directories that might be tracked but shouldn't be scanned
|
|
USE_GIT_LS_FILES=${USE_GIT_LS_FILES:-1}
|
|
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
# Filter out paths we don't want to scan even if tracked
|
|
# NUL-delimited end to end (git ls-files -z | grep -z | xargs -0) so a path
|
|
# with a space or newline is scanned as one file, not split into several.
|
|
GIT_FILES_FILTER="grep -zv '^migrations/versions/' | grep -zv '^frontend/dist/' | grep -zv '^staticdocs/' | grep -zv '\.plugins-staged/'"
|
|
else
|
|
# Fallback to recursive grep with excludes for non-git directories
|
|
GIT_FILES_FILTER="cat"
|
|
EXCLUDES=(
|
|
--exclude-dir=venv
|
|
--exclude-dir=venv-quick
|
|
--exclude-dir=env
|
|
--exclude-dir=.venv
|
|
--exclude-dir=node_modules
|
|
--exclude-dir=__pycache__
|
|
--exclude-dir=dist
|
|
# The installer's build output: a staged copy of the whole application plus
|
|
# a second SPA build under dist-subpath, which --exclude-dir=dist does not
|
|
# match. Linting it means linting vendored minified JS and failing on
|
|
# characters nobody in this repository wrote.
|
|
--exclude-dir=bundle
|
|
--exclude-dir=dist-subpath
|
|
--exclude-dir=.git
|
|
# Visual Studio's local state, gitignored. CopilotSnapshots keeps a full
|
|
# per-edit copy of touched source files, so every real finding is reported
|
|
# again once per snapshot - hundreds of duplicates that bury the actual
|
|
# hits and make the scan take minutes instead of seconds.
|
|
--exclude-dir=.vs
|
|
--exclude-dir=versions
|
|
--exclude-dir=staticdocs
|
|
--exclude-dir=_transfer
|
|
--exclude-dir=instance
|
|
# Local, gitignored scratch notes (see .gitignore devnotes/). Not product
|
|
# source, never committed, and free to hold pasted logs full of em-dashes
|
|
# and arrows - grep does not consult .gitignore, so exclude it explicitly.
|
|
--exclude-dir=devnotes
|
|
# .NET build output. The agent projects emit vendored runtime DLLs and
|
|
# generated *.nuget.g.props/.targets under bin/ and obj/; both are full of
|
|
# bytes nobody here wrote. Same failure as the .vs case above: thousands of
|
|
# hits that bury every real finding and make the gate unreadable, so it
|
|
# stops being run at all.
|
|
--exclude-dir=bin
|
|
--exclude-dir=obj
|
|
--exclude-dir=publish
|
|
--exclude-dir=dev-bin
|
|
# Data, not source: exported CSVs carry vendor mojibake and backups are
|
|
# snapshots of a moment. Neither is something a convention applies to.
|
|
--exclude-dir=exports
|
|
--exclude-dir=backups
|
|
)
|
|
fi
|
|
|
|
# DO NOT ADD --exclude=<file> TO THE ARRAY ABOVE.
|
|
#
|
|
# In this grep build a single file-level --exclude silently CANCELS EVERY
|
|
# --include filter on the same command. Bisected: with only --exclude-dir flags
|
|
# a shorthand scan restricted to --include='*.py' returns 0 markdown hits; add
|
|
# any one --exclude and the same scan returns 1142. Directory excludes are
|
|
# unaffected - it is specifically the file form.
|
|
#
|
|
# This is not theoretical, it was live: --exclude=findings.sarif and
|
|
# --exclude=report.md sat in this array and had been quietly disabling the
|
|
# include lists for every check that used them. That is why the shorthand check
|
|
# reported .md, .ps1, .cs, .json and .gitignore hits when its flags say it only
|
|
# ever looks at .py/.vue/.js/.ts, and why the gate accumulated thousands of
|
|
# unactionable findings that made it unreadable enough to be ignored.
|
|
#
|
|
# To skip a FILE, filter it out of the pipeline afterwards (see drop_scratch and
|
|
# the post-filters on the ADR-015 check) or exclude the directory holding it.
|
|
# Binaries are handled by grep -I on the scan itself.
|
|
|
|
# Root-level gitignored scratch: review writeups and chat handoffs, free to hold
|
|
# pasted logs full of em-dashes and arrows. grep does not consult .gitignore, so
|
|
# it has to be dropped here.
|
|
#
|
|
# A POST-FILTER, not --exclude-dir=devnotes. That flag matches a directory of
|
|
# that NAME at any depth, so a force-added plugins/<plugin>/devnotes/ would be
|
|
# invisible to every check in this file, ADR-015 included - a hole in the gate
|
|
# that nothing would report. This drops only the path we mean.
|
|
drop_scratch() { grep -v '^\./devnotes/' || true; }
|
|
|
|
# Generated scan output, replacing the --exclude=findings.sarif and
|
|
# --exclude=report.md that used to live in EXCLUDES. Same intent, but as a
|
|
# post-filter, because the flag form cancelled every --include on the command
|
|
# (see the warning above the EXCLUDES array). Also drops .env, which is local
|
|
# configuration rather than source.
|
|
#
|
|
# scan.log arrived from wj as --exclude=scan.log. Carried here rather than to
|
|
# the array for the same reason: the flag form would have cancelled the include
|
|
# lists it sat beside.
|
|
drop_generated() {
|
|
# Match with or without leading ./ for git ls-files compatibility
|
|
# [^:]* rather than .*: on a long minified hit line the greedy form blows
|
|
# PCRE's backtracking limit, and grep then errors instead of filtering.
|
|
grep -vP '^(\./)?([^:]*/)?(findings\.sarif|report\.md|scan\.log|\.env|[^:/]*\.standalone\.js|[^:/]*\.min\.js):' || true
|
|
}
|
|
|
|
# CONTRIBUTING.md documents the conventions by SHOWING violations as examples,
|
|
# so the naming checks hit it every time. Came from wj as
|
|
# --exclude=CONTRIBUTING.md; it is a post-filter here because the flag form
|
|
# cancels every --include on the same command (see the EXCLUDES warning above).
|
|
drop_contributing() { grep -v '^\./CONTRIBUTING\.md:' || true; }
|
|
|
|
INCLUDES_CODE=(
|
|
--include='*.py'
|
|
--include='*.vue'
|
|
--include='*.js'
|
|
--include='*.ts'
|
|
)
|
|
|
|
INCLUDES_ALL=(
|
|
--include='*.py'
|
|
--include='*.vue'
|
|
--include='*.js'
|
|
--include='*.ts'
|
|
--include='*.json'
|
|
--include='*.md'
|
|
--include='*.yaml'
|
|
--include='*.yml'
|
|
)
|
|
|
|
# INCLUDES_ALL, not INCLUDES_CODE: the check only ever covered .py/.vue/.js/.ts,
|
|
# so documentation was free to accumulate em-dashes, arrows and smart quotes -
|
|
# and did, including in files this repo's own convention forbids them in.
|
|
# Markdown, JSON and YAML are now covered too.
|
|
echo "==> Checking for non-ASCII characters..."
|
|
# -I skips binary files. Without it grep reports "Binary file X matches" for any
|
|
# archive or compiled artifact that slips past the excludes, which is noise no
|
|
# author can act on: there is no line to fix. The excludes above are the primary
|
|
# defence; this is the backstop for whatever they miss.
|
|
#
|
|
# wj wrote this as --binary-files=without-match, which is the same behaviour
|
|
# spelled long-hand; -I is kept for consistency with the rest of the file.
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
NON_ASCII=$(git ls-files -z \
|
|
'*.py' '*.vue' '*.js' '*.ts' '*.json' '*.md' '*.yaml' '*.yml' \
|
|
| eval "$GIT_FILES_FILTER" \
|
|
| xargs -0 -r grep -PnI '[^\x00-\x7F]' 2>/dev/null \
|
|
| drop_scratch | drop_generated || true)
|
|
else
|
|
NON_ASCII=$(grep -rPnI '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_ALL[@]}" . 2>/dev/null | drop_scratch | drop_generated || true)
|
|
fi
|
|
if [ -n "$NON_ASCII" ]; then
|
|
echo "FAIL: non-ASCII characters found (em-dashes, smart quotes, arrows, emojis):"
|
|
echo "$NON_ASCII"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
echo "==> Checking for banned shorthand (standalone)..."
|
|
# Match the word as a standalone identifier: not preceded or followed by underscore/word char
|
|
# Word boundary in grep is \b but we want to exclude suffix usage like printers_bp
|
|
# So: match (^|[^a-zA-Z0-9_])(banned)([^a-zA-Z0-9_]|$)
|
|
# Exclude: docs (CONTRIBUTING.md examples), external code (agent/, installer/), this script
|
|
# Only check PRODUCT CODE in shopdb/ plugins/ frontend/src/ - not docs, examples, or tools
|
|
# Exclude C# client code (.cs files) - they're not Python and have different conventions
|
|
for word in cfg ctx mgr req res; do
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
# :(glob) is required: without it git matches pathspecs with fnmatch,
|
|
# where 'shopdb/**/*.py' needs a second slash and silently skips
|
|
# top-level files such as shopdb/__init__.py and frontend/src/main.js.
|
|
HITS=$(git ls-files -z ':(glob)shopdb/**/*.py' ':(glob)shopdb/**/*.vue' ':(glob)shopdb/**/*.js' ':(glob)shopdb/**/*.ts' \
|
|
':(glob)plugins/**/*.py' ':(glob)plugins/**/*.vue' ':(glob)plugins/**/*.js' ':(glob)plugins/**/*.ts' \
|
|
':(glob)frontend/src/**/*.py' ':(glob)frontend/src/**/*.vue' ':(glob)frontend/src/**/*.js' ':(glob)frontend/src/**/*.ts' \
|
|
| grep -zv '/tests\?/' | grep -zv '\.spec\.js' | grep -zv '\.plugins-staged' \
|
|
| xargs -0 -r grep -Pn "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" 2>/dev/null \
|
|
| grep -v '\.cs:' \
|
|
| grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \
|
|
| drop_scratch | drop_generated || true)
|
|
else
|
|
HITS=$(grep -rPn --binary-files=without-match \
|
|
"(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" "${EXCLUDES[@]}" \
|
|
--include='*.py' --include='*.vue' --include='*.js' --include='*.ts' \
|
|
shopdb/ plugins/ frontend/src/ 2>/dev/null \
|
|
| grep -v '/tests\?/' \
|
|
| grep -v '\.spec\.js' \
|
|
| grep -v '\.plugins-staged' \
|
|
| grep -v '\.cs:' \
|
|
| grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \
|
|
| drop_scratch \
|
|
| drop_generated \
|
|
|| true)
|
|
fi
|
|
if [ -n "$HITS" ]; then
|
|
echo "FAIL: banned shorthand '$word' (standalone) found:"
|
|
echo "$HITS"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
done
|
|
|
|
echo "==> Checking for snake_case DB tablenames..."
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
SNAKE_TABLES=$(git ls-files -z '*.py' | eval "$GIT_FILES_FILTER" \
|
|
| xargs -0 -r grep -Pn "__tablename__\s*=\s*['\"][^'\"]*_" 2>/dev/null \
|
|
| drop_scratch | drop_contributing || true)
|
|
else
|
|
SNAKE_TABLES=$(grep -rPn "__tablename__\s*=\s*['\"][^'\"]*_" "${EXCLUDES[@]}" \
|
|
--include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true)
|
|
fi
|
|
if [ -n "$SNAKE_TABLES" ]; then
|
|
echo "FAIL: snake_case __tablename__ found (must be lowercase concatenated):"
|
|
echo "$SNAKE_TABLES"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
echo "==> Checking for snake_case DB column attrs..."
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
SNAKE_COLS=$(git ls-files -z '*.py' | eval "$GIT_FILES_FILTER" \
|
|
| xargs -0 -r grep -Pn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" 2>/dev/null \
|
|
| drop_scratch | drop_contributing || true)
|
|
else
|
|
SNAKE_COLS=$(grep -rPn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" "${EXCLUDES[@]}" \
|
|
--include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true)
|
|
fi
|
|
if [ -n "$SNAKE_COLS" ]; then
|
|
echo "FAIL: snake_case db.Column attribute found (must match column name, no underscores):"
|
|
echo "$SNAKE_COLS"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
echo "==> Checking for snake_case ForeignKey targets..."
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
SNAKE_FK=$(git ls-files -z '*.py' | eval "$GIT_FILES_FILTER" \
|
|
| xargs -0 -r grep -Pn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" 2>/dev/null \
|
|
| drop_scratch || true)
|
|
else
|
|
SNAKE_FK=$(grep -rPn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null | drop_scratch || true)
|
|
fi
|
|
if [ -n "$SNAKE_FK" ]; then
|
|
echo "FAIL: snake_case ForeignKey target found:"
|
|
echo "$SNAKE_FK"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
echo "==> Checking for snake_case API params in frontend (DB-mirrored fields)..."
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
SNAKE_FE=$(git ls-files -z '*.vue' '*.js' '*.ts' | eval "$GIT_FILES_FILTER" \
|
|
| grep -zv 'DEVELOPMENT-STANDARDS.md' \
|
|
| xargs -0 -r grep -Pn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" 2>/dev/null \
|
|
| drop_scratch || true)
|
|
else
|
|
SNAKE_FE=$(grep -rPn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" "${EXCLUDES[@]}" \
|
|
--exclude=DEVELOPMENT-STANDARDS.md --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null | drop_scratch || true)
|
|
fi
|
|
if [ -n "$SNAKE_FE" ]; then
|
|
echo "FAIL: snake_case API params in frontend (must match DB column names without underscores):"
|
|
echo "$SNAKE_FE"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
# ADR-013 Phase 4: a plugin frontend (plugins/<name>/frontend/) may import core
|
|
# via the @/ alias or its own tree (./, ../ within the plugin), never another
|
|
# plugin's tree and never an escaping ../../ into src. Keeps plugin frontends
|
|
# self-contained so a per-site build can drop one cleanly.
|
|
echo "==> Checking for cross-plugin / escaping imports in plugin frontends..."
|
|
if [ -d plugins ]; then
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
PLUGIN_FE_IMPORTS=$(git ls-files -z ':(glob)plugins/*/frontend/**/*.vue' ':(glob)plugins/*/frontend/**/*.js' \
|
|
| xargs -0 -r grep -Pn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" 2>/dev/null || true)
|
|
else
|
|
PLUGIN_FE_IMPORTS=$(grep -rPn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" \
|
|
--include='*.vue' --include='*.js' plugins/*/frontend/ 2>/dev/null || true)
|
|
fi
|
|
if [ -n "$PLUGIN_FE_IMPORTS" ]; then
|
|
echo "FAIL: plugin frontend imports must use the @/ alias for core, not"
|
|
echo " an escaping ../../ or another plugin's path:"
|
|
echo "$PLUGIN_FE_IMPORTS"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
fi
|
|
|
|
# ADR-015: one site's data does not belong in product code. A site host, a site
|
|
# FQDN or a site network in plugins/ or shopdb/ ships another site a value it
|
|
# cannot see and did not choose. Use a setting with a NEUTRAL default, a
|
|
# site-namespaced directory (scripts/site_imports/<site>/), or seed data.
|
|
#
|
|
# Code generation lives in one module. Seven views were importing qrcode and
|
|
# jsbarcode directly, each with its own margin, width and error correction, and
|
|
# the answers had already drifted - which on a label means one page scans and
|
|
# another does not. frontend/src/utils/codes.js owns that knowledge now; a view
|
|
# passes what is specific to its label and nothing else.
|
|
echo "==> Checking that only the shared module imports the code libraries..."
|
|
if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
CODE_LIB_IMPORTS=$(git ls-files -z ':(glob)frontend/src/**/*.vue' ':(glob)frontend/src/**/*.js' ':(glob)plugins/**/*.vue' ':(glob)plugins/**/*.js' \
|
|
| grep -zv '\.plugins-staged/' \
|
|
| xargs -0 -r grep -n "from ['\"]\(qrcode\|jsbarcode\)['\"]" 2>/dev/null \
|
|
| grep -v 'frontend/src/utils/codes\.js' || true)
|
|
else
|
|
CODE_LIB_IMPORTS=$(grep -rn "from ['\"]\(qrcode\|jsbarcode\)['\"]" \
|
|
--include='*.vue' --include='*.js' \
|
|
frontend/src plugins/ 2>/dev/null \
|
|
| grep -v 'frontend/src/utils/codes\.js' \
|
|
| grep -v '\.plugins-staged/' || true)
|
|
fi
|
|
if [ -n "$CODE_LIB_IMPORTS" ]; then
|
|
echo "FAIL: import qrcode/jsbarcode through @/utils/codes, not directly:"
|
|
echo "$CODE_LIB_IMPORTS"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
# ADR-017: a marker position is pixels in ONE LEVEL's coordinate space, so a
|
|
# payload that emits mapx/mapy without levelid gives the consumer coordinates and
|
|
# no drawing to put them on. The consumer then either renders nothing or - worse,
|
|
# and what a reasonable implementation does - falls back to the default level,
|
|
# drawing one building's ground floor behind a marker positioned for another
|
|
# building's mezzanine. That renders perfectly and points at the wrong place.
|
|
#
|
|
# Eight files emit positions across twenty sites. This is the check that says
|
|
# whether all of them travel with their level, because reading them by eye is
|
|
# how the twentieth gets missed.
|
|
echo "==> Checking that emitted map positions carry their level (ADR-017)..."
|
|
# PER OCCURRENCE, not per file. The file-level form passed a module that emitted
|
|
# 'mapx' six times and 'levelid' once, and two payloads shipped without a level:
|
|
# the toner report and the enforcement report, both feeding a hover preview that
|
|
# then said "no level". Every 'mapx' must have a 'levelid' in the same literal -
|
|
# the window is wide enough for a comment between them, and no wider.
|
|
MISSING_LEVEL=""
|
|
while IFS= read -r hit; do
|
|
[ -z "$hit" ] && continue
|
|
file=${hit%%:*}
|
|
line=${hit#*:}; line=${line%%:*}
|
|
# Python writes 'levelid', Vue writes levelid: - accept either, so the
|
|
# check covers the payloads a browser sends as well as the ones the server
|
|
# emits. It was Python-only, which is how three asset forms shipped a
|
|
# position with no level.
|
|
if ! sed -n "${line},$((line + 8))p" "$file" | grep -qE "'levelid'|levelid:"; then
|
|
MISSING_LEVEL="$MISSING_LEVEL$file:$line"$'\n'
|
|
fi
|
|
done <<EOF
|
|
$(grep -rn "'mapx':" --include='*.py' shopdb/ plugins/ scripts/ 2>/dev/null | grep -v '/tests\?/' || true)
|
|
$(grep -rn "mapx: " --include='*.vue' --include='*.js' \
|
|
frontend/src plugins/*/frontend 2>/dev/null \
|
|
| grep -v '.plugins-staged' | grep -v '\.spec\.js' \
|
|
| grep -vE "mapx: (null|0)," || true)
|
|
EOF
|
|
if [ -n "$MISSING_LEVEL" ]; then
|
|
echo "FAIL: these emit 'mapx' with no 'levelid' beside it - a position with no"
|
|
echo " level cannot be drawn on the right floor plan (ADR-017):"
|
|
echo "$MISSING_LEVEL" | sed 's/^/ /'
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
# The same rule for the hover preview: binding coordinates into
|
|
# LocationMapTooltip without a level makes it report "no level" for every asset,
|
|
# which is exactly what shipped in 0.11.0 - all seven call sites missed it.
|
|
TOOLTIP_MISSING=""
|
|
for candidate in $(grep -rl "LocationMapTooltip" --include='*.vue' frontend/src plugins/ 2>/dev/null | grep -v plugins-staged || true); do
|
|
grep -q ':left=' "$candidate" || continue
|
|
grep -q ':levelid=' "$candidate" || TOOLTIP_MISSING="$TOOLTIP_MISSING$candidate"$'\n'
|
|
done
|
|
if [ -n "$TOOLTIP_MISSING" ]; then
|
|
echo "FAIL: these bind LocationMapTooltip coordinates without :levelid, so the"
|
|
echo " preview cannot know which drawing to use (ADR-017):"
|
|
echo "$TOOLTIP_MISSING" | sed 's/^/ /'
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
|
|
# ENFORCING. It was report-only while the backlog was worked off, and the hit
|
|
# count then did not move for weeks - a rule that only prints is read as no rule.
|
|
# Set SITE_LITERALS_ENFORCE=0 to drop back to reporting for a local run.
|
|
SITE_LITERALS_ENFORCE=${SITE_LITERALS_ENFORCE:-1}
|
|
|
|
echo "==> Checking for site-specific literals in product code (ADR-015)..."
|
|
# A real site hostname, a site FQDN, a site name or a site network.
|
|
#
|
|
# SCOPE. It used to look at .py/.vue/.js under plugins/ and shopdb/ only, and
|
|
# every literal that actually reached a second site was somewhere else: the
|
|
# PowerShell clients, the installer, the seeds, generated JSON. Case-sensitive
|
|
# too, so a capitalised spelling of the host passed. Both fixed - the scan is
|
|
# only worth having where
|
|
# the leaks are.
|
|
#
|
|
# Fleet-wide vocabulary (gea-shopfloor-*) is NOT matched: it is overridable
|
|
# through the pctypemap settings and is not one site's data.
|
|
#
|
|
# A line may declare itself deliberate with a trailing `ADR-015-OK: <reason>`
|
|
# marker. That is for an organisation-wide default that is genuinely right for
|
|
# every site and configurable anyway - not for "we will fix it later". The
|
|
# marker makes the claim visible in review; silence would not.
|
|
# ASSEMBLED FROM FRAGMENTS, deliberately. This script is published, and the
|
|
# publication scrub greps for the very strings below - written literally, the
|
|
# rule's own definition trips the gate that enforces it. Same reason
|
|
# tests/test_docs_publishable.py splits its terms.
|
|
# Adjacent quoted strings CONCATENATE: 'ab''cd' is the four-character string
|
|
# abcd. The apostrophe-escape form ('ab'\''cd') looks almost identical and is
|
|
# not - it embeds a literal ' in the middle, so every pattern below matched
|
|
# nothing and this check silently passed everything from the day it was written
|
|
# until 2026-08-26. That is how a QA SaaS tenant sat in the shipped defaults for
|
|
# months with the gate green. Keep the fragments split (the scrub greps this
|
|
# file for the very strings the patterns are made of) and keep them ADJACENT.
|
|
SITE_HOST='tsg''wp00525'
|
|
SITE_DOMAIN='\.geaero''space\.net'
|
|
SITE_NAME='West Jeff''erson'
|
|
SITE_NETS='10\.134\.48\.|10\.48\.249\.'
|
|
# A named SaaS TENANT is as site-specific as a hostname, and this one shipped as
|
|
# the default ServiceNow URL for months: every adopting site would have linked
|
|
# its technicians into another organisation's QA instance. The vendor domain
|
|
# itself is fine to mention; the tenant is not.
|
|
SITE_TENANTS='geaero''spaceqa'
|
|
SITE_PATTERNS="${SITE_HOST}|${SITE_DOMAIN}|\bwjs\b|${SITE_NAME}|${SITE_NETS}|${SITE_TENANTS}"
|
|
SITE_HITS=$(grep -rPni --binary-files=without-match \
|
|
"$SITE_PATTERNS" "${EXCLUDES[@]}" \
|
|
--exclude=README.md \
|
|
--include='*.py' --include='*.vue' --include='*.js' \
|
|
--include='*.ps1' --include='*.psm1' --include='*.sh' --include='*.iss' \
|
|
--include='*.json' --include='*.html' \
|
|
plugins/ shopdb/ scripts/ deploy/ frontend/src/ tools/ 2>/dev/null \
|
|
| grep -v '/tests\?/' \
|
|
| grep -v '\.spec\.js:' \
|
|
| grep -v 'site_imports/' \
|
|
| grep -v 'installer/bundle/' \
|
|
| grep -v 'offline-packages/' \
|
|
| grep -v '\.plugins-staged/' \
|
|
| grep -v 'check-naming-and-style\.sh' \
|
|
| grep -v 'export-github\.sh' \
|
|
| grep -v 'ADR-015-OK' || true)
|
|
if [ -n "$SITE_HITS" ]; then
|
|
COUNT=$(echo "$SITE_HITS" | wc -l)
|
|
if [ "$SITE_LITERALS_ENFORCE" = "1" ]; then
|
|
echo "FAIL: $COUNT site-specific literal(s) in product code:"
|
|
echo "$SITE_HITS"
|
|
echo
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
else
|
|
echo " $COUNT site-specific literal(s) found (report only, not failing):"
|
|
echo "$SITE_HITS" | sed 's/^/ /'
|
|
echo " See docs/adr/ADR-015-site-specific-configuration.md"
|
|
echo
|
|
fi
|
|
fi
|
|
|
|
if [ "$VIOLATIONS" -gt 0 ]; then
|
|
echo "=================================================="
|
|
echo "$VIOLATIONS naming/style violation(s) found."
|
|
echo "See CONTRIBUTING.md for the full convention."
|
|
echo "=================================================="
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> All naming/style checks passed."
|
|
exit 0
|