Files
ScadaBridge/docs/deployment/deploy-prep/2026-11-12-gates-52-131/apply.sh
T
Joseph Doherty a92a418d7f docs(deploy): prepare the #52 / #131 site-script fixes for DEV and QA (not applied)
Read-only export of DEV vd03 and QA vq03 (2026-11-12): the only
Children["DelmiaRecv"] user is CvdReactor.ProcessRecipeDownload (script 4,
identical on both envs; DEV 3 / QA 9 instances), and the only ScriptAlarm
.Severity reader is DEV TestMesMachine.AlarmStatus (script 42, SBTEST01/02).
QA has no #131 work.

Adds the original (rollback) and fixed bodies, a guarded check/apply/rollback
script that re-sends every script field, and the apply plan: #52 any time,
#131 with or after the dd4f1c2a binary. The old #131 body still compiles on
main but returns banded values; the fixed body is what fails to compile on the
deployed binaries. Rig evidence for both gates is in the README.

Refs #52 #131

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 23:19:07 -05:00

101 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Apply / roll back the Gitea #52 and #131 site-script fixes on one ScadaBridge central.
# Prepared 2026-11-12; see README.md in this folder for the order relative to the binary deploy.
#
# SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 \
# SCADABRIDGE_USERNAME=... SCADABRIDGE_PASSWORD=... \
# bash apply.sh <check|apply|rollback> <52|131> [--no-deploy]
#
# check READ-ONLY. Prints, per target script, whether the live body is the ORIGINAL (deployed
# 2026-11-12), the FIXED body, or something else, and whether each instance is stale.
# apply Guarded update: refuses unless the live body is byte-identical to original/<script>.cs
# (or already fixed -> no-op). Re-sends every other field from original/<script>.meta.json
# (`template script update` REPLACES parameters/return-def/trigger-config — omitting them
# would wipe them). Then deploys the affected instances unless --no-deploy.
# rollback The reverse: requires the live body to be fixed/<script>.cs, writes original/<script>.cs.
#
# Credentials come from the environment only (SCADABRIDGE_USERNAME / SCADABRIDGE_PASSWORD); nothing
# is passed on argv. DEV vd03 runs DisableLogin (any value works); QA needs a real login.
#
# #52 -> template CvdReactor, script ProcessRecipeDownload; deploys every CvdReactor instance.
# #131 -> template TestMesMachine, script AlarmStatus; deploys every TestMesMachine instance
# (SBTEST01/SBTEST02, DEV only — QA has no TestMesMachine and no script reading .Severity).
HERE="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "$HERE/../../../.." && pwd)"
export SB_CLI="${SB_CLI:-$REPO/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge}"
export SB_URL="${SCADABRIDGE_URL:?set SCADABRIDGE_URL}" SB_HERE="$HERE"
export SB_MODE="${1:?check|apply|rollback}" SB_GATE="${2:?52|131}" SB_NODEPLOY="${3:-}"
: "${SCADABRIDGE_USERNAME:?set SCADABRIDGE_USERNAME}" "${SCADABRIDGE_PASSWORD:?set SCADABRIDGE_PASSWORD}"
python3 - <<'PY'
import json, os, subprocess, sys
CLI, URL, HERE = os.environ["SB_CLI"], os.environ["SB_URL"], os.environ["SB_HERE"]
MODE, GATE, NODEPLOY = os.environ["SB_MODE"], os.environ["SB_GATE"], os.environ["SB_NODEPLOY"] == "--no-deploy"
TARGET = {"52": ("CvdReactor", "ProcessRecipeDownload"), "131": ("TestMesMachine", "AlarmStatus")}[GATE]
KEY = f"{TARGET[0]}.{TARGET[1]}"
def cli(*args, timeout=300):
r = subprocess.run([CLI, *args, "--url", URL, "--format", "json"], capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise SystemExit(f"FAILED {' '.join(args[:3])}: {(r.stderr or r.stdout).strip()[:600]}")
return json.loads(r.stdout) if r.stdout.strip() else None
def body(kind): return open(os.path.join(HERE, kind, KEY + ".cs"), encoding="utf-8").read()
ORIG, FIXED = body("original"), body("fixed")
META = json.load(open(os.path.join(HERE, "original", KEY + ".meta.json"), encoding="utf-8"))
tmpl = next((t for t in cli("template", "list") if t["name"] == TARGET[0] and not t.get("parentTemplateId")), None)
if tmpl is None:
print(f"{URL}: no base template {TARGET[0]} — nothing to do for #{GATE}"); sys.exit(0)
detail = cli("template", "get", "--id", str(tmpl["id"]), "--detail")
script = next((s for s in detail["scripts"] if s["name"] == TARGET[1] and not s.get("isInherited")), None)
if script is None:
print(f"{URL}: template {TARGET[0]} has no own script {TARGET[1]}"); sys.exit(0)
live = script.get("code") or ""
state = "ORIGINAL" if live == ORIG else "FIXED" if live == FIXED else "UNKNOWN"
insts = [i for i in cli("instance", "list") if i["templateId"] == tmpl["id"]]
print(f"{URL}: {KEY} template={tmpl['id']} script={script['id']} live body = {state}")
for i in insts:
d = cli("instance", "diff", "--id", str(i["id"]))
print(f" instance {i['uniqueName']} (id {i['id']}) stale={d.get('isStale')} deployedAt={d.get('deployedAt')}")
if MODE == "check":
sys.exit(0)
want, need = (FIXED, "ORIGINAL") if MODE == "apply" else (ORIG, "FIXED")
if live == want:
print(" already in the target state — no script update")
if not any(cli("instance", "diff", "--id", str(i["id"])).get("isStale") for i in insts):
print(" every instance current — nothing to deploy"); sys.exit(0)
elif state != need:
raise SystemExit(f" REFUSED: live body is {state}, expected {need}. Diff it against {HERE} before touching it.")
else:
args = ["template", "script", "update", "--id", str(script["id"]), "--name", script["name"],
"--code", want, "--trigger-type", script["triggerType"]]
for opt, field in (("--trigger-config", "triggerConfiguration"), ("--parameters", "parameterDefinitions"),
("--return-def", "returnDefinition"), ("--min-time-between-runs", "minTimeBetweenRuns"),
("--execution-timeout-seconds", "executionTimeoutSeconds")):
v = META.get(field)
if v not in (None, ""):
args += [opt, str(v)]
if META.get("isLocked"):
args.append("--locked")
cli(*args)
after = next(s for s in cli("template", "get", "--id", str(tmpl["id"]), "--detail")["scripts"] if s["id"] == script["id"])
for field in ("parameterDefinitions", "returnDefinition", "triggerConfiguration"):
if (after.get(field) or None) != (META.get(field) or None):
raise SystemExit(f" {field} changed by the update — restore it from original/{KEY}.meta.json")
if (after.get("code") or "") != want:
raise SystemExit(" body read back does not match the target file")
print(f" script {script['id']} updated ({MODE}); metadata read back unchanged")
if NODEPLOY:
print(" --no-deploy: instances left stale; deploy them yourself"); sys.exit(0)
for i in insts:
r = cli("instance", "deploy", "--id", str(i["id"]))
print(f" deploy {i['uniqueName']}: {json.dumps(r)[:200]}")
PY