diff --git a/README.md b/README.md index 5f98881..50b94d6 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ Web 容器的 Nginx 配置通过 bind mount 挂载自仓库文件 [docker/nginx. docker compose -f docker-compose.yml restart web ``` +## 生产 CI/CD + +Gitea Actions 会对 Pull Request 和 `main` Push 执行完整质量门禁,并以源码完整 Git SHA 构建 API/Web 镜像。`ai.51easyai.com` 只接受来自 `main` 历史的语义版本 Tag 发布,迁移前自动备份数据库,失败时恢复上一应用镜像。安装 Runner、发布、验证和回滚步骤见 [生产 CI/CD 运行手册](docs/runbooks/production-ci-cd.md),设计取舍见 [ADR-001](docs/decisions/001-production-cicd.md)。 + Compose 默认使用独立容器数据库 `postgres:18-alpine`,数据卷会保留在 `postgres_data` 和 `api_data`。为避免本地开发 `.env` 中的 `localhost` 数据库地址污染容器部署,compose 使用 `AI_GATEWAY_COMPOSE_*` 变量作为容器部署专用覆盖,例如: ```bash diff --git a/docs/decisions/001-production-cicd.md b/docs/decisions/001-production-cicd.md new file mode 100644 index 0000000..d9ae60f --- /dev/null +++ b/docs/decisions/001-production-cicd.md @@ -0,0 +1,51 @@ +# ADR-001: 使用仓库级 Runner 和不可变镜像发布生产环境 + +## 状态 + +Accepted + +## 日期 + +2026-07-17 + +## 背景 + +AI Gateway 已通过独立部署仓库和 Docker Compose 手工运行在 `110.42.51.33`,公共入口是 `https://ai.51easyai.com`。同一服务器还运行认证中心、K3s 和其他 EasyAI 服务,磁盘空间有限。Gitea 1.22 的 host Runner 已知不能可靠启动第二个依赖 Job,也不完整支持 GitHub Actions 的 `environment`、`permissions` 和 `concurrency` 语义。 + +生产发布必须满足:源码可追溯、数据库迁移前有备份、失败可恢复上一应用版本、Secret 不进入仓库或流水线日志,并且不能让普通分支自动修改生产环境。 + +## 决策 + +- 为 `BCAI/easyai-ai-gateway` 注册独立、仓库级 host Runner。它不与认证中心 Runner 共享身份、状态目录或任务标签。 +- Pull Request 和 `main` Push 执行相同质量门禁与镜像构建;只有符合 `vMAJOR.MINOR.PATCH` 形式、且目标提交属于 `main` 历史的 Tag 才发布生产。 +- API/Web 镜像使用完整 Git SHA 作为运行时 Tag。语义版本 Tag 只是发布授权信号,不作为可变镜像标识。 +- 验证和发布处于同一个 Runner Job。这样部署只使用已通过全部门禁的同一 checkout,并规避当前 Gitea 版本的依赖 Job 领取问题。 +- Runner 使用专属 Buildx builder,缓存上限为 2 GB。服务器 root 现有的阿里云 Registry 凭据只由固定发布助手使用,不复制到 Gitea Secret。 +- 发布助手在迁移前生成 PostgreSQL custom-format 备份并验证目录;保留最近 5 份。应用健康检查失败时恢复上一组 API/Web 镜像。 +- 数据库迁移必须保持向后兼容。自动回滚只覆盖应用镜像;数据库恢复需要人工确认,避免覆盖失败发布后产生的有效写入。 +- Gitea Runner 的 Docker 权限等价于宿主机 root 权限。因此只允许受信任的仓库维护者触发该 Runner,来自 Fork 的 Pull Request 必须先经维护者批准。 + +## 备选方案 + +### 复用认证中心 Runner + +拒绝。现有 Runner 是认证中心仓库级身份,扩大为组织级 Runner 会让更多仓库获得同一 Docker/root 信任边界。 + +### 由 Runner 保存 SSH 或 Registry Secret + +拒绝。Runner 已位于目标服务器,绕回 SSH 会增加长期凭据;复制 Registry Secret 也会扩大暴露面。固定 root helper 可以复用服务器现有登录状态。 + +### `main` 通过后直接发布生产 + +拒绝。`ai.51easyai.com` 是生产入口。语义版本 Tag 提供明确的发布授权点,同时仍保留自动化与可审计性。 + +### 只依赖 `latest` + +拒绝。`latest` 无法证明运行中的容器对应哪个源码提交,也不能可靠选择回滚版本。 + +## 影响 + +- 日常合并不会自动改变生产;创建并推送版本 Tag 才会部署。 +- 首次启用 CD 时需要在服务器捕获当前运行镜像作为 bootstrap 回滚基线。 +- 发布前必须确保数据库迁移对上一应用版本保持兼容。 +- 如果未来增加不受信任的贡献者,应把 CI 构建迁移到隔离的 rootless Runner,并保留独立的受保护部署 Runner。 diff --git a/docs/runbooks/production-ci-cd.md b/docs/runbooks/production-ci-cd.md new file mode 100644 index 0000000..e8d3eb5 --- /dev/null +++ b/docs/runbooks/production-ci-cd.md @@ -0,0 +1,90 @@ +# 生产 CI/CD 运行手册 + +## 流水线约定 + +- Runner:`easyai-gateway-act-runner.service` +- Runner 标签:`easyai-gateway-linux:host` +- Runner 状态:`/var/lib/easyai-gateway-runner` +- 工具链:`/opt/easyai-gateway-ci` +- Gitea:`https://git.51easyai.com/BCAI/easyai-ai-gateway` +- 生产入口:`https://ai.51easyai.com` +- 部署目录:`/root/easyai-ai-gateway-deploy` + +PR、`main` Push 和版本 Tag 都会执行格式、Go vet/test、govulncheck、前端 lint/test/build、依赖审计、Compose 校验、Trivy 扫描与 API/Web 镜像构建。只有版本 Tag 会执行最后的生产发布步骤。 + +## 首次安装或修复 Runner + +从 Gitea 仓库设置的 Actions Runner 页面取得一次性仓库注册 Token,在服务器可信 checkout 中执行: + +```bash +RUNNER_REGISTRATION_TOKEN='<一次性 Token>' ./scripts/provision-ci-runner.sh +``` + +Token 只通过进程环境传入。生成的 `/var/lib/easyai-gateway-runner/.runner` 必须保持 `0600`,不得复制到仓库、报告或聊天记录。 + +验证: + +```bash +systemctl is-active easyai-gateway-act-runner.service +systemctl is-enabled easyai-gateway-act-runner.service +journalctl -u easyai-gateway-act-runner.service --since '15 minutes ago' +``` + +Runner 使用 `daemon --once`。每个 Job 结束后进程自然退出,再由 systemd 启动新的长轮询进程。 + +## 发布生产版本 + +1. 合并到 `main`,等待 `ci / verify (push)` 成功。 +2. 在已验证的 `main` 提交上创建语义版本 Tag。 +3. 推送 Tag,等待同一 CI Job 完成镜像扫描、发布、数据库备份、迁移和公共健康检查。 + +```bash +git switch main +git pull --ff-only +git tag -a v0.1.1 -m 'release: v0.1.1' +git push origin v0.1.1 +``` + +流水线会部署 Tag 指向提交的完整 Git SHA,而不是 `latest` 或版本号镜像。 + +## 发布后验证 + +```bash +curl -fsS https://ai.51easyai.com/gateway-api/healthz +curl -fsS https://ai.51easyai.com/gateway-api/readyz +ssh root@110.42.51.33 'cd /root/easyai-ai-gateway-deploy && ./gateway-ops.sh ps' +``` + +同时检查服务器磁盘、API 日志和数据库容器健康状态。发布后的首小时如出现错误率翻倍、P95 延迟增加 50%、数据完整性或安全问题,应立即回滚。 + +## 应用回滚 + +发布失败时脚本会自动恢复 `.release.env` 中的上一组 API/Web 镜像并重复健康检查。人工回滚到一个仍存在于本机或 Registry 的历史 Git SHA: + +```bash +sudo /usr/local/sbin/easyai-ai-gateway-release <40 位历史 Git SHA> +``` + +该操作也会先创建数据库备份。不要把 `latest`、分支名或版本号传给发布助手。 + +## 数据库恢复 + +备份位于 `/var/backups/easyai-ai-gateway`,默认只保留最近 5 份。应用回滚不会自动恢复数据库。只有确认需要回退数据且已经停止写入后,才能由操作员选择备份执行 `pg_restore`;恢复前必须再保留当前数据库副本。 + +## Runner 故障恢复 + +重启前先确认没有构建、扫描或部署子进程: + +```bash +ps -u easyai-gateway-runner -o pid,ppid,etime,cmd --forest +systemctl status easyai-gateway-act-runner.service --no-pager +``` + +确认空闲后才执行: + +```bash +systemctl restart easyai-gateway-act-runner.service +journalctl -u easyai-gateway-act-runner.service -n 50 --no-pager +``` + +不要因为日志暂时没有新增就用定时器重启 Runner;镜像构建和扫描可长时间无输出。