titan-iac/scripts/ops/hermes_code_demo.sh
jenkins 35ae4d4bab
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 142
refactor(demo): split the two demos into two scripts
One script with a 'fixture' and a 'code' subcommand meant the wrong subcommand
was always one word away, in front of an audience, with different blast radii
behind each: the triage demo touches only a ConfigMap, the code demo pushes to
a repository and deletes issues. Those should not share a command line.

Each demo is now its own driver with the same five verbs - monitor, reset,
preflight, run, status - so knowing one teaches the other. What they genuinely
share (credentials, Jenkins access, the tick reader, the lab-wide preflight
checks) moved to hermes_demo_lib.sh rather than being duplicated, because the
reason to split was clarity at the command line, not two copies of the same
helper drifting apart.

Each reset now covers only its own demo. The triage reset no longer reaches
into a Gitea repository it never writes to, and the code reset owns the
repository cleanup entirely.

The credentials file is now hermes_demo.env since both read it; the old
hermes_triage_demo.env is still sourced as a fallback so a filled-in file
keeps working, and both names stay git-ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:59:23 -03:00

187 lines
7.6 KiB
Bash
Executable File

#!/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