All checks were successful
plugin-tests / test (push) Successful in 25s
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>
126 lines
4.6 KiB
Python
Executable File
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()
|