Repository navigation
147 lines (139 loc) · 6.94 KB
/
Copy pathci.yml
File metadata and controls
147 lines (139 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
name: CI
on:
push:
branches: [main]
pull_request:
# 最小权限:CI 只需要读代码(checkout),不发布任何制品。
# GitHub Actions 的默认 GITHUB_TOKEN 是 repo 写权限,被恶意 workflow 改写
# (或第三方 action 被投毒)时能直接推 main;只读权限把爆炸半径限到零。
permissions: read-all
# 同分支推送/PR 只保留最新的那一轮 CI:hotfix 连推几发时旧 run 全部取消,
# 不再空烧 runner 额度。
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
backend:
runs-on: ubuntu-latest
# 挂死保险丝:覆盖率全量跑 ≈ 4 分钟,给 15 分钟足够;无此限一个死循环
# 测试会烧 6 小时(GitHub 默认 360 分钟)。
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
- run: uv sync --frozen
- name: Ruff
run: uv run ruff check src tests scripts
- name: 测试与覆盖率(门槛 100%)
run: uv run pytest -q --cov=src --cov-report=term --cov-fail-under=100
security:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
- run: uv sync --frozen
- name: bandit(中危及以上,配置见 pyproject [tool.bandit])
run: uvx bandit -c pyproject.toml -r src scripts -ll
- name: pip-audit(已知漏洞,含 dev 依赖)
# 必须用 `uv run --with`:`uvx pip-audit` 会在**自己的临时环境**里跑,
# 扫到的是 pip-audit 的依赖树(实测 28 个包,含 pip-audit/rich/requests),
# 项目依赖(fastapi/uvicorn/httpx/cryptography)一个都不在内 —— 门一直是
# 绿的,但没有任何防线。`uv run --with` 注入项目 .venv,实测扫到 58 个包。
run: uv run --with pip-audit pip-audit --skip-editable
frontend:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- name: 依赖漏洞(生产依赖,high 及以上阻断)
run: |
# registry.npmjs.org 的审计端点偶发 ERR_SOCKET_TIMEOUT(实测连续两次
# 在 2 分钟超时后失败,同期 registry 本身可正常拉包)。重试 3 次:
# 真漏洞会稳定复现、三次都非零;网络抖动大概率第二次就过。
for i in 1 2 3; do
if pnpm audit --prod --audit-level=high; then exit 0; fi
echo "::warning::pnpm audit 第 $i 次失败(可能是 registry 网络抖动),15s 后重试"
sleep 15
done
exit 1
- run: pnpm exec tsc --noEmit
- run: pnpm exec vitest run
- run: pnpm build
compose:
runs-on: ubuntu-latest
# 真实构建 + 启动探活(pnpm install ×2 与 docker build),最重的一环
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 校验 compose 语法
run: docker compose config --quiet
env:
APP_SECRET: ci-placeholder-secret
- name: 校验 compose 公网覆盖文件语法(Caddy 入口,两种 TLS 模式)
env:
APP_SECRET: ci-placeholder-secret
run: |
# acme(域名):PUBLIC_DOMAIN + ACME_EMAIL
PUBLIC_DOMAIN=ai-api.example.com ACME_EMAIL=ops@example.com \
docker compose -f docker-compose.yml -f docker-compose.public.yml config --quiet
# internal(公网 IP、自签):不设 ACME_EMAIL——自签模式用不到,必须能拼出来
PUBLIC_DOMAIN=203.0.113.10 TLS_MODE=internal \
docker compose -f docker-compose.yml -f docker-compose.public.yml config --quiet
# 缺 PUBLIC_DOMAIN 必须报错(覆盖文件里 :? 断言的回归保护)
if docker compose -f docker-compose.yml -f docker-compose.public.yml config --quiet 2>/dev/null; then
echo "::error::缺 PUBLIC_DOMAIN 时 compose 本应报错,却通过了"
exit 1
fi
- name: 校验 Caddyfile 语法(域名 / 自签两种模式)
run: |
# 只 validate、不起服务:占位域名不申请证书(validate 不联网签发)
for mode in acme internal; do
case "$mode" in
acme) domain=ai-api.example.com; email=ops@example.com ;;
# 自签模式站址是公网 IP,且刻意不传 ACME_EMAIL(tls_internal 片段不引用它)
internal) domain=203.0.113.10; email= ;;
esac
docker run --rm \
-e PUBLIC_DOMAIN="$domain" -e ACME_EMAIL="$email" \
-e PORT=8000 -e TLS_MODE="$mode" \
-v "$PWD/deploy/caddy:/etc/caddy:ro" \
caddy:2.11.7-alpine caddy validate --config /etc/caddy/Caddyfile
done
- name: 构建镜像并启动(验证 compose 可真实运行)
run: |
mkdir -p data secrets
# 容器以 uid 1001 (appuser) 运行,挂载目录必须可写,
# 否则 SQLite 打不开、服务崩溃重启,health 永远不通
sudo chown -R 1001:1001 data secrets
docker build -t coding2api:ci .
# 用户名必须是 admin:compose 的 ADMIN_USERNAMES 默认 admin,
# B5 引导要求至少一个活跃 admin,否则启动直接 Traceback。
printf 'admin:%s\n' "$(docker run --rm coding2api:ci python -c 'from src.auth.users import create_password_hash;print(create_password_hash("cipw"))')" > secrets/users.txt
# --build 必须带:否则 compose 会去 registry 拉 image: 指向的已发布镜像,
# 测的就不是本次构建的代码了(曾因此长期静默通过)。
APP_SECRET=ci-placeholder-secret docker compose up -d --build
# 120s:启动预热会真连上游拉模型列表(zen 免费层还要逐个探活)。
for _ in $(seq 1 60); do
if curl -fsS http://127.0.0.1:8000/health >/dev/null; then break; fi
sleep 2
done
# 失败排查线索:容器日志(含启动 Traceback)
docker logs coding2api --tail 80
curl -fsS http://127.0.0.1:8000/health
APP_SECRET=ci-placeholder-secret docker compose down