187 lines
7.6 KiB
Bash
187 lines
7.6 KiB
Bash
|
|
#!/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
|