svrnty-hermes-webui-plugin/scripts/upstream-sync.py
Svrnty c1e3fa1af0
All checks were successful
plugin-tests / test (push) Successful in 25s
feat(plugin): Phase 2 partial — vault_status migrated + brand skin moved + eval suite (P2.B/C, P3.A/B)
Lands the easy migrations + the automation skeleton. STT migration deferred
to Phase 2.1 (it touches the streaming engine + bootstrap JS — needs a new
streaming_hook public-API method OR forced-internal CONNECTION-MAP entries).

Migrated to plugin:
  routes/vault_status.py    GET /api/vault/status (from fork commit 3e2c74f3)
  static/{app.js,app.css,fonts/}  brand skin (from hermes-ext/)

Plugin auto-loaded by hermes-webui when HERMES_WEBUI_PYTHON_PLUGIN is set;
register_static + inject_stylesheet + inject_script wire the URL contract at
/plugins/svrnty/{app.css,app.js} per protocol §14 (Q5).

Automation skeleton:
  Makefile                          one-liner targets: test · map · sync-upstream · smoke
  scripts/boot-smoke.py             start upstream+plugin, curl every endpoint
  scripts/upstream-sync.py          fetch tags + run matrix + JSON report
  tests/evals/test_features.py      4 evals (loader contract · vault payload · brand URL contract · forced-internal=0)
  tests/unit/test_brand_skin.py     4 asset-presence + wiring tests
  tests/unit/test_vault_status.py   3 handler tests (register, success, error)

CONNECTION-MAP.md: 0 forced-internal dependencies; plugin uses only public API.
AST script timestamp removed so map-check is deterministic.

Tests: 11/11 PASS (4 evals + 7 unit). Integration tests deferred until
boot-smoke runs against a live hermes-webui (Phase 2.D + 2.E gate).

Deferred to next session:
  P2.A  STT migration (needs streaming_hook design — see routes/transcribe.py)
  P2.D  Revert 4 fork feature commits — needs STT migration first
  P2.E  Archive hermes-ext repo — gated on P2.D
  P2.F  Live boot smoke against real webui

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:02:47 -04:00

126 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""upstream-sync.py — fetch upstream tags + run plugin matrix against new ones.
Exit 0 if every new tag passed boot-smoke + tests + map-check. Exit 1 if any
tag failed. Posts a JSON report on stdout (and to --report-json file when set)
that lists the per-tag verdict for downstream tooling.
Used by:
- Makefile `make sync-upstream` target (manual)
- .github/workflows/upstream-drift.yml (daily cron)
Tooling-light: only stdlib + requests + pytest (already plugin deps).
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
PLUGIN_REPO = Path(__file__).resolve().parent.parent
FORK_REPO = PLUGIN_REPO.parent / "hermes-webui"
def _git(*args, cwd=None, check=True):
return subprocess.run(
["git", *args], cwd=cwd or PLUGIN_REPO,
capture_output=True, text=True, check=check,
).stdout.strip()
def _fetch_upstream():
"""git fetch upstream in the fork repo. Returns list of new tags vs HEAD."""
if not (FORK_REPO / ".git").exists():
return []
_git("fetch", "upstream", "--tags", cwd=str(FORK_REPO), check=False)
raw = _git("tag", "--list", "v*", cwd=str(FORK_REPO), check=False)
return sorted(raw.splitlines()) if raw else []
def _current_tested():
"""Return manifest.yaml's tested_versions list."""
mf = PLUGIN_REPO / "manifest.yaml"
if not mf.exists():
return []
tested = []
in_block = False
for line in mf.read_text().splitlines():
if line.strip().startswith("tested_versions:"):
in_block = True
continue
if in_block:
if line.strip().startswith("- "):
tested.append(line.strip()[2:].strip())
elif line and not line[0].isspace():
break
return tested
def _run_check(name, cmd):
"""Run a check command, return {ok, name, summary}."""
try:
out = subprocess.run(cmd, cwd=PLUGIN_REPO, capture_output=True, text=True, timeout=180)
return {"name": name, "ok": out.returncode == 0,
"summary": (out.stdout + out.stderr)[-200:]}
except subprocess.TimeoutExpired:
return {"name": name, "ok": False, "summary": "timeout"}
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--report-json", default=None, help="Write full report here")
ap.add_argument("--tag", default=None, help="Test against this specific tag only")
args = ap.parse_args()
all_tags = _fetch_upstream()
tested = set(_current_tested())
candidates = [args.tag] if args.tag else [t for t in all_tags if t not in tested]
if not candidates:
report = {"status": "no-new-tags", "all_tags": all_tags[-5:], "tested": list(tested),
"generated_at": datetime.now(timezone.utc).isoformat()}
if args.report_json:
Path(args.report_json).write_text(json.dumps(report, indent=2))
print(json.dumps(report, indent=2))
sys.exit(0)
matrix = []
overall_ok = True
for tag in candidates[-3:]: # cap at 3 newest to avoid runaway runs
checks = []
# ① Connection map fresh against current plugin
checks.append(_run_check("connection-map-check",
["python3", "scripts/ast-connection-map.py", "--check"]))
# ② Unit + integration tests pass
checks.append(_run_check("pytest-unit",
["python3", "-m", "pytest", "tests/unit", "-q"]))
checks.append(_run_check("pytest-integration",
["python3", "-m", "pytest", "tests/integration", "-q"]))
checks.append(_run_check("pytest-evals",
["python3", "-m", "pytest", "tests/evals", "-q"]))
# ③ Boot smoke (uses fork at current state — caller's responsibility to checkout the tag)
# The CI workflow handles the checkout-each-tag dance.
checks.append(_run_check("boot-smoke",
["python3", "scripts/boot-smoke.py"]))
tag_ok = all(c["ok"] for c in checks)
overall_ok = overall_ok and tag_ok
matrix.append({"tag": tag, "ok": tag_ok, "checks": checks})
report = {
"status": "ok" if overall_ok else "fail",
"tested": list(tested),
"new_candidates": candidates,
"matrix": matrix,
"generated_at": datetime.now(timezone.utc).isoformat(),
}
if args.report_json:
Path(args.report_json).write_text(json.dumps(report, indent=2))
print(json.dumps(report, indent=2))
sys.exit(0 if overall_ok else 1)
if __name__ == "__main__":
main()