titan-iac/scripts/ops/hermes_code_demo.sh

187 lines
7.6 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Drive and narrate the Hermes code-proposal demo.
#
# This is the proposal loop: a build fails on a real defect, Ariadne asks
# Hermes for a minimal patch, validates it as data, pushes a branch and opens a
# pull request. Nothing merges. The point of this half is the stop, not the fix.
#
# hermes_code_demo.sh monitor # follow the Test Automation Diagram live
# hermes_code_demo.sh reset # restore the demo repository to pre-run state
# hermes_code_demo.sh preflight # confirm the lab is ready to demo
# hermes_code_demo.sh run # seed the defect and narrate the loop
# hermes_code_demo.sh status # current incident/alert state, no changes
#
# The autonomous-repair demo is a separate script: hermes_triage_demo.sh.
#
# FIRST RUN: copy hermes_demo.env.example to hermes_demo.env in this directory
# and fill it in. That file is git-ignored precisely so it can hold real
# tokens; this script sources it automatically. You also need bstein/
# hermes-code-demo cloned locally (default ~/Development/hermes-code-demo,
# override with CODE_REPO_DIR).
#
# The only thing this mutates is the demo repository: it pushes a seeded defect
# to master and reverts it on reset.
set -euo pipefail
# shellcheck source=scripts/ops/hermes_demo_lib.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hermes_demo_lib.sh"
# DEMO_REPOS is the whole blast radius and is deliberately explicit: a real
# service's issues are genuine triage records, and clearing them to tidy a demo
# would destroy the evidence the system exists to produce.
DEMO_REPOS="${DEMO_REPOS:-hermes-code-demo}"
cmd_reset() {
say "Reset — restoring the code demo to its pre-run state"
if [ -z "${GITEA_TOKEN:-}" ]; then
note "GITEA_TOKEN unset; skipping repository cleanup"
else
for repo in $DEMO_REPOS; do
note "clearing bstein/$repo (demo repository)"
local items
items="$(gitea_get "/api/v1/repos/bstein/$repo/issues?state=all&limit=100" |
python3 -c 'import json,sys
for i in json.load(sys.stdin):
print(i["number"], "pr" if i.get("pull_request") else "issue")' 2>/dev/null || true)"
if [ -z "$items" ]; then
note " no issues or pull requests"
else
while read -r num kind; do
[ -z "$num" ] && continue
curl -s --max-time 25 -o /dev/null -X DELETE -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/bstein/$repo/issues/$num"
note " deleted $kind #$num"
done <<< "$items"
fi
local branches
branches="$(gitea_get "/api/v1/repos/bstein/$repo/branches" |
python3 -c 'import json,sys,urllib.parse
for b in json.load(sys.stdin):
if b["name"].startswith("hermes-repair/"):
print(urllib.parse.quote(b["name"], safe=""))' 2>/dev/null || true)"
if [ -z "$branches" ]; then
note " no repair branches"
else
for ref in $branches; do
curl -s --max-time 25 -o /dev/null -X DELETE -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/bstein/$repo/branches/$ref"
note " deleted branch $(printf '%b' "${ref//%/\\x}")"
done
fi
done
fi
# The demo seeds its defect by pushing to master, and the fix only lands if
# someone merges the pull request - which, by design, nobody does during a
# demo. So master stays broken, and the next run aborts on "defect already
# present". Reset has to undo the seed rather than just report it, or the
# second demo of the day fails before it starts.
if [ -d "$CODE_REPO_DIR/.git" ]; then
note "restoring the demo repository working state"
( cd "$CODE_REPO_DIR" && git checkout -q master && git fetch -q origin &&
git reset -q --hard origin/master ) || note " could not sync master"
if grep -q 'percent / 100' "$CODE_REPO_DIR/src/discount.py" 2>/dev/null; then
note " src/discount.py is correct; demo is armable"
else
note " src/discount.py carries the seeded defect; reverting it on master"
( cd "$CODE_REPO_DIR" &&
python3 - <<'PY'
import pathlib, re, sys
path = pathlib.Path("src/discount.py")
source = path.read_text()
# Matches the seeded `percent / 10` without also matching a correct
# `percent / 100`, so re-running reset on a healthy file changes nothing.
fixed = re.sub(r"percent / 10(?!\d)", "percent / 100", source)
if fixed == source:
sys.exit("unrecognised defect; fix src/discount.py by hand")
path.write_text(fixed)
PY
git commit -qam "revert: restore the discount divisor" && git push -q origin master &&
note " reverted and pushed; demo is armable" ) || note " revert failed — fix src/discount.py by hand"
fi
else
note "demo repository not cloned at $CODE_REPO_DIR; skipping"
fi
say "Ready"
note "real service repositories were not touched"
note "run 'preflight' next, then 'run'"
}
cmd_preflight() {
require_jenkins
say "Preflight — code demo"
if [ -d "$CODE_REPO_DIR/.git" ]; then
if grep -q 'percent / 100' "$CODE_REPO_DIR/src/discount.py" 2>/dev/null; then
note "demo repository: src/discount.py is correct; armable"
else
note "demo repository: src/discount.py carries a defect — run 'reset' first"
fi
else
note "demo repository: NOT CLONED at $CODE_REPO_DIR"
fi
local open_prs
open_prs="$(gitea_get "/api/v1/repos/bstein/hermes-code-demo/pulls?state=open" 2>/dev/null |
python3 -c 'import json,sys; print(len(json.load(sys.stdin)))' 2>/dev/null || echo '?')"
note "open hermes-code-demo PRs: $open_prs (must be 0 — the duplicate guard refuses while one is open)"
note "code proposals enabled: $(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_CODE_ENABLED 2>/dev/null)"
note "fix categories: $(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_FIX_CATEGORIES 2>/dev/null)"
shared_preflight
}
cmd_status() {
shared_status
say "Open proposals"
note "https://scm.bstein.dev/bstein/hermes-code-demo/pulls"
}
cmd_run() {
require_jenkins
[ -d "$CODE_REPO_DIR/.git" ] || { echo "clone bstein/hermes-code-demo to $CODE_REPO_DIR first" >&2; exit 1; }
local start_num next_num
start_num="$(last_build_number "$CODE_JOB")"
next_num=$((start_num + 1))
say "Seeding a one-line defect in src/discount.py"
( cd "$CODE_REPO_DIR" && git checkout -q master && git pull -q &&
python3 - <<'PY'
import pathlib
p = pathlib.Path("src/discount.py")
s = p.read_text()
old, new = "percent / 100", "percent / 10"
if old not in s:
raise SystemExit("defect already present or file changed; run reset first")
p.write_text(s.replace(old, new))
PY
git commit -qam "refactor: simplify discount percentage math" && git push -q origin master )
note "pushed: a plausible-looking change that breaks three regression tests"
say "Running the test gate -> build #$next_num"
note "HTTP $(jenkins_post "/job/$CODE_JOB/build")"
note "result: $(wait_for_build "$CODE_JOB" "$next_num")"
say "Ariadne collects evidence and asks Hermes for a minimal patch"
note "Hermes returns an anchored patch as data; Ariadne validates path, size,"
note "changed lines, and that the anchor is unique, then pushes hermes-repair/$next_num"
for _ in $(seq 1 40); do
sleep 15
ariadne_ticks 300 1 | grep -q "code_fix_proposed" && break
done
ariadne_ticks 400 3
say "Pull request awaiting human review (nothing merges automatically)"
note "https://scm.bstein.dev/bstein/hermes-code-demo/pulls"
}
case "${1:-}" in
run|code) cmd_run ;;
status) cmd_status ;;
preflight) cmd_preflight ;;
reset) cmd_reset ;;
monitor) run_monitor "$CODE_JOB" ;;
*) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//' ; exit 1 ;;
esac