ci: add minimal CI gate (pre-push hook + ci_gate.py + dormant Gitea workflow) [v2026.07.18.5]
CI / gate (push) Has been cancelled
CI / gate (push) Has been cancelled
This commit is contained in:
1 parent
dd10245ad5
commit
4167e23e50
4 files changed
+197
-8
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"""Minimal CI gate for qihuaiflow.
|
||||
|
||||
This is the canonical, single-source-of-truth gate. It runs the two test
|
||||
suites that guard production behavior, INSIDE the long-lived qihu-aiflow
|
||||
container (our deployment model keeps the app running in a container, not
|
||||
built fresh in CI):
|
||||
|
||||
1. backend pytest -> /app/backend/python/.venv/bin/python -m pytest -c pytest.ini
|
||||
2. frontend vitest -> /app/frontend && npx vitest run
|
||||
|
||||
Exit codes:
|
||||
0 all suites green -> deploy/push may proceed
|
||||
1 a test suite failed -> BLOCK (do not deploy/push)
|
||||
2 environment error -> docker missing or container not running
|
||||
|
||||
It auto-detects where it runs:
|
||||
* If the qihu-aiflow container is reachable via LOCAL docker (i.e. on the
|
||||
deploy host), it execs directly.
|
||||
* Otherwise it falls back to ssh_helper (remote control from a laptop),
|
||||
which reaches the host over SSH and execs into the container there.
|
||||
|
||||
Usage:
|
||||
python scripts/ci_gate.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CONTAINER = "qihu-aiflow"
|
||||
|
||||
BACKEND_CMD = "cd /app/backend/python && .venv/bin/python -m pytest -c pytest.ini -q"
|
||||
FRONTEND_CMD = "cd /app/frontend && npx vitest run --reporter=dot"
|
||||
|
||||
|
||||
def shell_quote(s: str) -> str:
|
||||
return "'" + s.replace("'", "'\\''") + "'"
|
||||
|
||||
|
||||
def local_runner(cmd: str):
|
||||
"""Run via local docker (deploy host)."""
|
||||
p = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return p.stdout, p.stderr, p.returncode
|
||||
|
||||
|
||||
def get_runner():
|
||||
# Prefer local docker ONLY if the actual container is reachable here
|
||||
# (avoids a Windows-docker edge case where `docker ps` works but the
|
||||
# qihu-aiflow container lives on a different host).
|
||||
try:
|
||||
r = subprocess.run(
|
||||
"docker ps --format '{{.Names}}'",
|
||||
shell=True, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if r.returncode == 0 and CONTAINER in r.stdout.split():
|
||||
return local_runner
|
||||
except Exception:
|
||||
pass
|
||||
# Fall back to SSH control of the real host.
|
||||
try:
|
||||
import ssh_helper as s # type: ignore
|
||||
return lambda cmd: s.run(cmd)
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"[CI Gate] ERROR: docker unavailable locally and ssh_helper missing: {e}",
|
||||
file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def run_in_container(runner, cmd: str):
|
||||
return runner(f"docker exec {CONTAINER} bash -c {shell_quote(cmd)}")
|
||||
|
||||
|
||||
def container_up(runner) -> bool:
|
||||
out, _, _ = runner(f"docker ps --format {shell_quote('{{.Names}}')}")
|
||||
return CONTAINER in out.split()
|
||||
|
||||
|
||||
def main():
|
||||
runner = get_runner()
|
||||
if not container_up(runner):
|
||||
print(f"[CI Gate] ERROR: container {CONTAINER} is not running", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
failures = []
|
||||
|
||||
print("=== [CI Gate] backend pytest ===")
|
||||
o, e, rc = run_in_container(runner, BACKEND_CMD)
|
||||
sys.stdout.write(o)
|
||||
if e.strip():
|
||||
print("STDERR:", e.strip(), file=sys.stderr)
|
||||
if rc != 0:
|
||||
failures.append("backend pytest")
|
||||
print(f"[CI Gate] backend pytest FAILED (rc={rc})", file=sys.stderr)
|
||||
else:
|
||||
print("[CI Gate] backend pytest OK")
|
||||
|
||||
print("=== [CI Gate] frontend vitest ===")
|
||||
o, e, rc = run_in_container(runner, FRONTEND_CMD)
|
||||
sys.stdout.write(o)
|
||||
if e.strip():
|
||||
print("STDERR:", e.strip(), file=sys.stderr)
|
||||
if rc != 0:
|
||||
failures.append("frontend vitest")
|
||||
print(f"[CI Gate] frontend vitest FAILED (rc={rc})", file=sys.stderr)
|
||||
else:
|
||||
print("[CI Gate] frontend vitest OK")
|
||||
|
||||
if failures:
|
||||
print(f"\n[CI Gate] BLOCKED - failed: {', '.join(failures)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("\n[CI Gate] PASSED - all suites green")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# qihuaiflow minimal CI gate - git pre-push hook.
|
||||
#
|
||||
# Runs backend pytest + frontend vitest INSIDE the long-lived qihu-aiflow
|
||||
# container before any `git push` leaves this host. If a suite fails the
|
||||
# push is blocked (exit non-zero), so a broken tree can never be published.
|
||||
#
|
||||
# Install (on the deploy host, inside the repo):
|
||||
# cp scripts/git-hooks/pre-push .git/hooks/pre-push
|
||||
# chmod +x .git/hooks/pre-push
|
||||
#
|
||||
# This is a self-contained bash gate (no python dependency on the host).
|
||||
# The canonical, documented version lives in scripts/ci_gate.py.
|
||||
set -uo pipefail
|
||||
|
||||
CONTAINER="qihu-aiflow"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||
echo "[CI Gate] ERROR: container $CONTAINER is not running - aborting push" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "=== [CI Gate] backend pytest ==="
|
||||
docker exec "$CONTAINER" bash -c 'cd /app/backend/python && .venv/bin/python -m pytest -c pytest.ini -q'
|
||||
backend_rc=$?
|
||||
if [ "$backend_rc" -ne 0 ]; then
|
||||
echo "[CI Gate] backend pytest FAILED (rc=$backend_rc) - push blocked" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[CI Gate] backend pytest OK"
|
||||
|
||||
echo "=== [CI Gate] frontend vitest ==="
|
||||
docker exec "$CONTAINER" bash -c 'cd /app/frontend && npx vitest run --reporter=dot'
|
||||
frontend_rc=$?
|
||||
if [ "$frontend_rc" -ne 0 ]; then
|
||||
echo "[CI Gate] frontend vitest FAILED (rc=$frontend_rc) - push blocked" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[CI Gate] frontend vitest OK"
|
||||
|
||||
echo "[CI Gate] PASSED - all suites green - push allowed"
|
||||
exit 0
|
||||
Reference in new issue
Block a user