#!/bin/bash set -euo pipefail # Live gate for the PER-SITE ARTIFACT REVISION that backs the pending-updates indicator # (design docs/plans/2026-09-14-pending-updates-design.md, plan task 10). Drives the real # CLI -> Traefik -> active central -> ArtifactDeploymentService path, so every assertion # below is end-to-end against the running rig. # # Steps: # A site list carries artifactRevisionHash / artifactsDeployedAt (the columns are live) # CAVEAT: the management API serializer omits null properties, so on a site that has # never had an artifact deploy the two keys are ABSENT rather than present-and-null. # A therefore asserts the shape (absent, or a well-formed sha256) and B is the proof. # B deploy artifacts fleet-wide -> every site stamped sha256:<64 hex>, deployed-at is now # C create a throwaway shared script -> hashes UNCHANGED until a deploy, then ALL CHANGED # D delete it, redeploy -> every hash is byte-identical to B again (determinism) # E Central UI /admin/sites answers 200 and serves the Artifacts badge markup (soft) # # NOTE on step E: the Sites page badge and the rail-footer indicator are Blazor Server # interactive content — with login ENABLED the badge text only lands after the circuit's # post-render LoadUpdatesAsync, which curl never sees. E is therefore a reachability + # static-markup smoke only; the real browser coverage is # tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PendingUpdatesTests.cs. # # EVERY read of the site list is checked for both a failed CLI call and a short row count # (read_rows below). Without that an empty list would pass the whole gate vacuously: "no # rows" means no malformed hash to report and no difference between two row sets. # # Usage: bash docker/verify-pending-updates.sh # SCADABRIDGE_URL=http://localhost:9000 bash docker/verify-pending-updates.sh SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" CLI="${SCADABRIDGE_CLI:-$PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge}" URL="${SCADABRIDGE_URL:-http://localhost:9000}" A=(--url "$URL" --format=json --username "${SCADABRIDGE_USERNAME:-multi-role}" --password "${SCADABRIDGE_PASSWORD:-password}") PROBE_NAME="PendingUpdatesProbe_$(date +%s)" PASS=0; FAIL=0 pass() { PASS=$((PASS+1)); echo " PASS $*"; } fail() { FAIL=$((FAIL+1)); echo " FAIL $*"; } # Bail out mid-run without losing the tally. Used for the failures that make every later # assertion meaningless (no sites, an unreachable management API, a short row read). abort() { echo ""; echo "PASS $PASS / FAIL $FAIL"; exit 1; } [ -x "$CLI" ] || { echo "CLI not found at $CLI — build it: dotnet build src/ZB.MOM.WW.ScadaBridge.CLI"; exit 1; } # The CLI's JSON envelope varies by command (bare array vs {"data": [...]}); normalise once. UNWRAP=' import json,sys d=json.load(sys.stdin) if isinstance(d, dict): d = d.get("data", d) ' # Prints one "\t\t\t" line per site, id-sorted # NUMERICALLY. Exits non-zero if the CLI call fails (pipefail) or the list is empty. site_rows() { "$CLI" site list "${A[@]}" | python3 -c "$UNWRAP"' import sys if not d: sys.exit(2) for s in sorted(d, key=lambda x: int(x["id"])): print("%s\t%s\t%s\t%s" % (s["id"], s.get("siteIdentifier"), s.get("artifactRevisionHash") or "-", s.get("artifactsDeployedAt") or "-")) ' } # read_rows