Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4167e23e50 | ||
|
|
dd10245ad5 |
No files matched your search
@@ -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'
|
||||||
@@ -3,19 +3,23 @@
|
|||||||
# 版本号格式: vYYYY.MM.DD.N (年.月.日.当日递增序号,从 1 开始)
|
# 版本号格式: vYYYY.MM.DD.N (年.月.日.当日递增序号,从 1 开始)
|
||||||
# 环境标签: prod = 正式版 | test = 测试版
|
# 环境标签: prod = 正式版 | test = 测试版
|
||||||
|
|
||||||
VERSION=v2026.07.18.1
|
VERSION=v2026.07.18.5
|
||||||
ENV=prod
|
ENV=prod
|
||||||
DATE=2026-07-18
|
DATE=2026-07-18
|
||||||
BASE_IMAGE=qihu-aiflow:with-deps
|
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)
|
## 本版本变更 (v2026.07.18.5 / prod)
|
||||||
- Phase0 工程纪律止血 启动
|
- Phase0 收官:新增「最小 CI 门禁」
|
||||||
- 新增「部署前镜像备份 SOP」:scripts/backup_before_deploy.py,每次部署前自动提交递增序号镜像快照
|
- scripts/ci_gate.py:统一测试网关,容器内跑后端 pytest + 前端 vitest,自动判定本地 docker 或经 ssh_helper 远程执行,任一失败即非零退出阻断发布
|
||||||
- 统一版本号规范 vYYYY.MM.DD.N + prod/test 标签(取代旧 -recover 标签)
|
- scripts/git-hooks/pre-push:纯 bash 的 git pre-push 钩子,push 前在容器内跑两套测试,失败即阻断 push(已安装到 .git/hooks)
|
||||||
- 引入后端 pytest / 前端 vitest 测试基建,作为迭代式重构的安全网
|
- .gitea/workflows/ci.yml:Gitea Actions 最小 CI 模板(当前无 act_runner,处于 dormant;钩子已先行拦截坏推送)
|
||||||
- 固化反复问题的回归用例 R1/R2/R3
|
- 测试安全网现状:后端 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,便于正式版推广后与测试版做环境切割。
|
- 旧方案:镜像曾用 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。
|
- 历史功能:AI聊天(real API + tool-call function calling)、知识库文件存储 + AI 图谱、频道二维码、邮件/短信配置 + AI 生成、支付配置、平台驾驶舱(token 累计 + 审计/操作日志)、Agent 资源 API、evolution pipeline 自动恢复、全局默认模型、广告位图片上传 + AI 绘图、应用市场安装/卸载/搜索/去重、资产文件类型分类 + 在线预览。迁移 001-015。
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.4.21"
|
"vite": "^5.4.21",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { formatBjTime } from './datetime';
|
||||||
|
|
||||||
|
// 北京时间渲染曾是反复出现的问题(微信Bot时间错乱、Token驾驶舱时间列不正确等)。
|
||||||
|
// 该测试固化「后端按北京时间存储、前端显式按 Asia/Shanghai 渲染」的契约。
|
||||||
|
describe('formatBjTime', () => {
|
||||||
|
it('空输入返回占位符 -', () => {
|
||||||
|
expect(formatBjTime(undefined)).toBe('-');
|
||||||
|
expect(formatBjTime(null)).toBe('-');
|
||||||
|
expect(formatBjTime('')).toBe('-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('无时区标记的时间戳按 Asia/Shanghai (+08:00) 渲染', () => {
|
||||||
|
// 2026-07-18T12:00:00 视为北京时间,渲染应含 12:00:00
|
||||||
|
const out = formatBjTime('2026-07-18T12:00:00');
|
||||||
|
expect(out).toContain('2026');
|
||||||
|
expect(out).toContain('12:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UTC(Z) 时间戳正确转换为北京时间 (+8)', () => {
|
||||||
|
// 2026-07-18T04:00:00Z = 北京时间 12:00:00
|
||||||
|
const out = formatBjTime('2026-07-18T04:00:00Z');
|
||||||
|
expect(out).toContain('12:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('非法日期原样返回', () => {
|
||||||
|
expect(formatBjTime('not-a-date')).toBe('not-a-date');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig } from 'vite';
|
/// <reference types="vitest/config" />
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -17,8 +18,6 @@ export default defineConfig({
|
|||||||
chunkSizeWarningLimit: 1200,
|
chunkSizeWarningLimit: 1200,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// 仅将 react 核心库单独打包(长期缓存);
|
|
||||||
// 其余第三方库交给 Rollup 自动按需拆分,避免被强制合并成一个大 vendor 在首屏加载
|
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (id.includes('node_modules')) {
|
if (id.includes('node_modules')) {
|
||||||
if (
|
if (
|
||||||
@@ -37,4 +36,9 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
},
|
},
|
||||||
|
// Phase 0 测试基建:vitest 配置(纯函数测试用 node 环境,组件测试可改 jsdom)
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -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