diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..9c603c1 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,28 @@ +# qihuaiflow minimal CI (Gitea Actions) +# +# NOTE: This workflow is DORMANT until a Gitea act_runner is registered with +# this repo. The active gate today is the host-side pre-push hook +# (scripts/git-hooks/pre-push) + scripts/ci_gate.py, because our deployment +# model keeps the app running in a long-lived `qihu-aiflow` container on the +# docker host rather than building it fresh in CI. +# +# To activate later: +# 1. Install act_runner on the docker host (sharing the docker socket). +# 2. Register it against this repo with label `ubuntu-latest`. +# 3. The runner can then `docker exec qihu-aiflow ...` to reach the app. +# +# Until then, the pre-push hook already blocks broken pushes. +name: CI +on: + push: + branches: [main] + pull_request: + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - name: Backend pytest + run: docker exec qihu-aiflow bash -c 'cd /app/backend/python && .venv/bin/python -m pytest -c pytest.ini -q' + - name: Frontend vitest + run: docker exec qihu-aiflow bash -c 'cd /app/frontend && npx vitest run --reporter=dot' diff --git a/VERSION b/VERSION index 61f8348..e580627 100644 --- a/VERSION +++ b/VERSION @@ -3,19 +3,23 @@ # 版本号格式: vYYYY.MM.DD.N (年.月.日.当日递增序号,从 1 开始) # 环境标签: prod = 正式版 | test = 测试版 -VERSION=v2026.07.18.1 +VERSION=v2026.07.18.5 ENV=prod DATE=2026-07-18 BASE_IMAGE=qihu-aiflow:with-deps -BACKUP_IMAGE=qihu-aiflow:backup-20260718-001 +BACKUP_IMAGE=qihu-aiflow:backup-20260718-005 -## 本版本变更 (v2026.07.18.1 / prod) -- Phase0 工程纪律止血 启动 -- 新增「部署前镜像备份 SOP」:scripts/backup_before_deploy.py,每次部署前自动提交递增序号镜像快照 -- 统一版本号规范 vYYYY.MM.DD.N + prod/test 标签(取代旧 -recover 标签) -- 引入后端 pytest / 前端 vitest 测试基建,作为迭代式重构的安全网 -- 固化反复问题的回归用例 R1/R2/R3 +## 本版本变更 (v2026.07.18.5 / prod) +- Phase0 收官:新增「最小 CI 门禁」 +- scripts/ci_gate.py:统一测试网关,容器内跑后端 pytest + 前端 vitest,自动判定本地 docker 或经 ssh_helper 远程执行,任一失败即非零退出阻断发布 +- scripts/git-hooks/pre-push:纯 bash 的 git pre-push 钩子,push 前在容器内跑两套测试,失败即阻断 push(已安装到 .git/hooks) +- .gitea/workflows/ci.yml:Gitea Actions 最小 CI 模板(当前无 act_runner,处于 dormant;钩子已先行拦截坏推送) +- 测试安全网现状:后端 9 passed(smoke/market/sms + R1/R2/R3),前端 4 passed(datetime 时区格式化) ## 历史 +- v2026.07.18.4 (prod):前端 vitest 基建落地(4 passed,datetime 时区格式化)。 +- v2026.07.18.3 (prod):回归用例 R1/R2/R3(9 passed)— 微信Bot截断、技能/MCP覆盖规则、媒体生成超时回写。 +- v2026.07.18.2 (prod):后端 pytest 基建落地(6 passed)— 冒烟/市场14类/短信宝MD5。 +- v2026.07.18.1 (prod):Phase0 启动 — 部署前镜像备份 SOP + 版本号规范 + 引入测试基建。 - 旧方案:镜像曾用 AIFlow v2.0.1 / v2026.07.14 / v2026.07.14-nav;git 曾用 v2026.07.12-recover / v2026.07.12-ui-recover / v2026.07.13-recover。自 v2026.07.18.1 起统一为 vYYYY.MM.DD.N + prod/test,便于正式版推广后与测试版做环境切割。 - 历史功能:AI聊天(real API + tool-call function calling)、知识库文件存储 + AI 图谱、频道二维码、邮件/短信配置 + AI 生成、支付配置、平台驾驶舱(token 累计 + 审计/操作日志)、Agent 资源 API、evolution pipeline 自动恢复、全局默认模型、广告位图片上传 + AI 绘图、应用市场安装/卸载/搜索/去重、资产文件类型分类 + 在线预览。迁移 001-015。 diff --git a/scripts/ci_gate.py b/scripts/ci_gate.py new file mode 100644 index 0000000..77d5bbe --- /dev/null +++ b/scripts/ci_gate.py @@ -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() diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100644 index 0000000..afde86a --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -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