Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 142
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>
187 lines
7.6 KiB
Bash
Executable File
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
|