scripts/prod_automation_status.sh took a positional since value, so the
Makefile's own `SINCE=sunday` form passed the literal string into timestamptz:
all five sections errored while the run still looked like it produced a report.
Now `SINCE=` is accepted and the value is parsed and normalised to UTC up
front, failing fast with usage and exit 2 instead of five parse errors.
ACTIVITY-WP-0032 plans adoption of the glas-harness contract 1.0 reported in
GLAS-WP-0004. WP-0026 is finished, so this gets its own plan. The motivation is
concrete: ops_run.approach_hint binds at claim time, which produced a failed
run on 2026-08-17 ("no approach matched labels/definition") after it had
already consumed a claim and a lease.
T01 is deliberately blocking: our claim path is pull-based and the Glas
contract is a call, so the invocation shape is an architectural decision, not a
port. Two questions are outstanding with glas-harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.5 KiB
Bash
Executable file
148 lines
5.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ACTIVITY-WP-0021-T08: production automation evidence without workstation DB.
|
|
# Usage:
|
|
# ./scripts/prod_automation_status.sh [since]
|
|
# since: "sunday" | ISO timestamp (default: sunday Europe/Berlin as UTC floor)
|
|
set -euo pipefail
|
|
|
|
SINCE_ARG="${1:-sunday}"
|
|
SSH_HOST="${PROD_AUTOMATION_SSH_HOST:-railiance01}"
|
|
NS="${PROD_AUTOMATION_NS:-activity-core}"
|
|
|
|
# The Makefile target is `make prod-automation-status SINCE=sunday`, so operators
|
|
# reasonably type the same `SINCE=` form when calling the script directly. Accept
|
|
# it rather than passing the literal into timestamptz, where every section fails
|
|
# with a separate parse error while the run still looks like it produced a report.
|
|
if [[ "$SINCE_ARG" == SINCE=* ]]; then
|
|
SINCE_ARG="${SINCE_ARG#SINCE=}"
|
|
fi
|
|
|
|
usage() {
|
|
cat >&2 <<'USAGE'
|
|
Usage: ./scripts/prod_automation_status.sh [since]
|
|
since: "sunday" (default) | an ISO-8601 timestamp, e.g. 2026-08-17T00:00:00Z
|
|
Read-only. Requires SSH to the production host.
|
|
USAGE
|
|
}
|
|
|
|
if [[ "$SINCE_ARG" == "sunday" ]]; then
|
|
# Floor of most recent Sunday 00:00 Europe/Berlin in UTC (portable enough for ops).
|
|
SINCE_UTC="$(python3 - <<'PY'
|
|
from datetime import datetime, timedelta, timezone
|
|
from zoneinfo import ZoneInfo
|
|
tz = ZoneInfo("Europe/Berlin")
|
|
now = datetime.now(tz)
|
|
days_back = (now.weekday() + 1) % 7
|
|
sunday = (now - timedelta(days=days_back)).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
print(sunday.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z"))
|
|
PY
|
|
)"
|
|
else
|
|
# Validate before the value reaches five separate SQL/JSON consumers: an
|
|
# unparseable `since` must fail fast and loudly, not produce a report whose
|
|
# every section is an error the reader has to notice.
|
|
if ! SINCE_UTC="$(python3 - "$SINCE_ARG" <<'PYVALIDATE'
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
raw = sys.argv[1].strip()
|
|
try:
|
|
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
sys.exit(1)
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
print(parsed.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z"))
|
|
PYVALIDATE
|
|
)"; then
|
|
echo "error: could not parse since value: ${SINCE_ARG}" >&2
|
|
usage
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
echo "=== prod automation status (railiance01) since ${SINCE_UTC} ==="
|
|
echo "host=${SSH_HOST} namespace=${NS}"
|
|
echo
|
|
|
|
ssh -o BatchMode=yes -o ConnectTimeout=15 "${SSH_HOST}" "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
|
echo '--- API health ---'
|
|
kubectl -n ${NS} exec deploy/actcore-api -- /app/.venv/bin/python3 -c 'import urllib.request; print(urllib.request.urlopen(\"http://127.0.0.1:8010/health\").read().decode())'
|
|
echo
|
|
echo '--- runs by activity ---'
|
|
kubectl -n ${NS} exec actcore-app-db-0 -- psql -U actcore -d actcore -c \"
|
|
SELECT d.name, d.enabled, count(*) AS runs,
|
|
min(coalesce(r.fired_at,r.scheduled_for)) AS first_fire,
|
|
max(coalesce(r.fired_at,r.scheduled_for)) AS last_fire,
|
|
sum(r.tasks_spawned) AS tasks
|
|
FROM activity_runs r
|
|
JOIN activity_definitions d ON d.id = r.activity_id
|
|
WHERE coalesce(r.scheduled_for, r.fired_at) >= timestamptz '${SINCE_UTC}'
|
|
GROUP BY d.name, d.enabled
|
|
ORDER BY runs DESC, d.name;
|
|
\"
|
|
echo
|
|
echo '--- execution outcomes by activity ---'
|
|
kubectl -n ${NS} exec actcore-app-db-0 -- psql -U actcore -d actcore -c \"
|
|
SELECT d.name, o.state, count(*) AS runs,
|
|
min(o.created_at) AS first_created,
|
|
max(o.updated_at) AS last_updated
|
|
FROM ops_runs o
|
|
JOIN activity_definitions d ON d.id = o.activity_definition_id
|
|
WHERE o.created_at >= timestamptz '${SINCE_UTC}'
|
|
GROUP BY d.name, o.state
|
|
ORDER BY d.name, o.state;
|
|
\"
|
|
echo
|
|
echo '--- failed or unclaimed execution detail (bounded) ---'
|
|
kubectl -n ${NS} exec actcore-app-db-0 -- psql -U actcore -d actcore -c \"
|
|
SELECT o.created_at, d.name, o.state, o.attempt, o.claim_owner,
|
|
left(o.title, 72) AS title,
|
|
left(coalesce(o.result->>'error', o.result->>'reason', ''), 240) AS failure
|
|
FROM ops_runs o
|
|
JOIN activity_definitions d ON d.id = o.activity_definition_id
|
|
WHERE o.created_at >= timestamptz '${SINCE_UTC}'
|
|
AND o.state IN ('failed', 'open', 'claimed')
|
|
ORDER BY o.created_at DESC
|
|
LIMIT 30;
|
|
\"
|
|
echo
|
|
echo '--- daily triage validation evidence (bounded) ---'
|
|
kubectl -n ${NS} exec deploy/actcore-api -- /app/.venv/bin/python3 -c '
|
|
import json, urllib.parse, urllib.request
|
|
from datetime import datetime
|
|
|
|
since = datetime.fromisoformat(\"${SINCE_UTC}\")
|
|
url = \"http://actcore-statehub-edge-relay:8000/progress/?\" + urllib.parse.urlencode(
|
|
{\"event_type\": \"daily_triage\", \"limit\": 100}
|
|
)
|
|
items = json.load(urllib.request.urlopen(url, timeout=10))
|
|
rows = []
|
|
for item in items:
|
|
created_raw = item.get(\"created_at\")
|
|
if not created_raw:
|
|
continue
|
|
created = datetime.fromisoformat(created_raw.replace(\"Z\", \"+00:00\"))
|
|
if created < since:
|
|
continue
|
|
detail = item.get(\"detail\") or {}
|
|
rows.append({
|
|
\"created_at\": created_raw,
|
|
\"run_id\": detail.get(\"activity_core_run_id\"),
|
|
\"output_validated\": detail.get(\"output_validated\"),
|
|
\"summary\": str(item.get(\"summary\") or \"\")[:200],
|
|
\"validation_error\": str(detail.get(\"validation_error\") or \"\")[:240],
|
|
})
|
|
print(json.dumps(rows[:14], indent=2))
|
|
'
|
|
echo
|
|
echo '--- non-high-frequency fires ---'
|
|
kubectl -n ${NS} exec actcore-app-db-0 -- psql -U actcore -d actcore -c \"
|
|
SELECT d.name, r.fired_at, r.tasks_spawned, r.run_id
|
|
FROM activity_runs r
|
|
JOIN activity_definitions d ON d.id = r.activity_id
|
|
WHERE coalesce(r.scheduled_for, r.fired_at) >= timestamptz '${SINCE_UTC}'
|
|
AND d.name NOT IN ('State Hub Consistency Sweep', 'Hourly RecentlyOnScope Reports')
|
|
ORDER BY r.fired_at;
|
|
\"
|
|
"
|