From 68ddff10654843929e5fa670ab0b8ce7f3dbc966 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:12:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor(infra):=20=E9=87=8D=E6=9E=84=E4=B8=BA?= =?UTF-8?q?=20no-push=20=E6=9C=AC=E5=9C=B0=E6=9E=84=E5=BB=BA=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 6 个分散 workflow(ci-ts/ci-go/ci-proto/ci-py/docker/deploy) - 合并为单个 ci.yml:3 个 quality job 并行 + deploy job 串行 - 全部使用官方镜像(node:22-alpine/golang:1.22-alpine/bufbuild/buf/docker:25-git) - deploy job 用 DooD 模式挂载 /var/run/docker.sock - docker-compose.deploy.yml 改用 build: 替代 image:(no-push) - 新增 docker-compose.tools.yml 一次性预拉所有 CI 镜像 - 支持 workflow_dispatch 指定 commit_sha 回滚 - 更新 project_rules §15 与 cicd-runbook.md 为 no-push 模式 - 新增设计文档 docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md --- .github/workflows/ci-go.yml | 60 --- .github/workflows/ci-proto.yml | 25 - .github/workflows/ci-py.yml | 33 -- .github/workflows/ci-ts.yml | 98 ---- .github/workflows/ci.yml | 197 ++++++++ .github/workflows/deploy.yml | 145 ------ .github/workflows/docker.yml | 92 ---- .trae/rules/project_rules.md | 106 ++-- docs/standards/cicd-runbook.md | 453 ++++++------------ ...6-07-08-cicd-no-push-local-build-design.md | 137 ++++++ infra/docker-compose.deploy.yml | 24 +- infra/docker-compose.tools.yml | 55 +++ 12 files changed, 613 insertions(+), 812 deletions(-) delete mode 100644 .github/workflows/ci-go.yml delete mode 100644 .github/workflows/ci-proto.yml delete mode 100644 .github/workflows/ci-py.yml delete mode 100644 .github/workflows/ci-ts.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/docker.yml create mode 100644 docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md create mode 100644 infra/docker-compose.tools.yml diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml deleted file mode 100644 index 8b9498e..0000000 --- a/.github/workflows/ci-go.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: CI Go - -# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml -# Go 服务分两阶段: -# - quality: golang:1.22-alpine 容器(不需要 docker) -# - docker-build: dockerreg.eazygame.cn/node-with-docker:22 + setup-go(需要 docker 命令) -# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7 - -on: - push: - branches: [main] - paths: - - 'services/api-gateway/**' - - 'services/push-gateway/**' - - 'packages/shared-go/**' - - 'go.work' - - '.github/workflows/ci-go.yml' - pull_request: - branches: [main] - paths: - - 'services/api-gateway/**' - - 'services/push-gateway/**' - - 'packages/shared-go/**' - - 'go.work' - - '.github/workflows/ci-go.yml' - -jobs: - quality: - runs-on: ubuntu-latest - container: golang:1.22-alpine - defaults: - run: - working-directory: services/api-gateway - steps: - - uses: actions/checkout@v4 - - - name: Download deps - run: go mod download - - - name: Vet - run: go vet ./... - - - name: Build - run: go build ./... - - - name: Test - run: go test ./... -v -coverprofile=coverage.out - - docker-build: - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - needs: quality - steps: - - uses: actions/checkout@v4 - - name: Build api-gateway - run: | - docker build \ - -t edu/api-gateway:ci \ - -f services/api-gateway/Dockerfile \ - services/api-gateway diff --git a/.github/workflows/ci-proto.yml b/.github/workflows/ci-proto.yml deleted file mode 100644 index 0788e0c..0000000 --- a/.github/workflows/ci-proto.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: CI Proto - -on: - push: - branches: [main] - paths: - - 'packages/shared-proto/**' - pull_request: - branches: [main] - paths: - - 'packages/shared-proto/**' - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: bufbuild/buf-setup-action@v1 - - name: buf lint - working-directory: packages/shared-proto - run: buf lint - - name: buf breaking - if: github.event_name == 'pull_request' - working-directory: packages/shared-proto - run: buf breaking --against .git#branch=main,subdir=packages/shared-proto diff --git a/.github/workflows/ci-py.yml b/.github/workflows/ci-py.yml deleted file mode 100644 index 5d93f59..0000000 --- a/.github/workflows/ci-py.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: CI Python - -on: - push: - branches: [main] - paths: - - 'services/data-ana/**' - - 'services/ai/**' - - 'packages/shared-py/**' - - 'pyproject.toml' - pull_request: - branches: [main] - paths: - - 'services/data-ana/**' - - 'services/ai/**' - - 'packages/shared-py/**' - - 'pyproject.toml' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - uses: astral-sh/setup-uv@v3 - - name: Install - run: uv sync - - name: Lint (ruff) - run: uv run ruff check . - - name: Test - run: uv run pytest diff --git a/.github/workflows/ci-ts.yml b/.github/workflows/ci-ts.yml deleted file mode 100644 index 562d349..0000000 --- a/.github/workflows/ci-ts.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: CI TypeScript - -# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml -# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(带 docker 的 node 22) -# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7 - -on: - push: - branches: [main] - paths: - - 'services/classes/**' - - 'apps/**' - - 'packages/**' - - 'scripts/**' - - 'package.json' - - 'pnpm-workspace.yaml' - - 'tsconfig.base.json' - - '.github/workflows/ci-ts.yml' - pull_request: - branches: [main] - paths: - - 'services/classes/**' - - 'apps/**' - - 'packages/**' - - 'scripts/**' - - 'package.json' - - 'pnpm-workspace.yaml' - - 'tsconfig.base.json' - - '.github/workflows/ci-ts.yml' - -env: - SKIP_ENV_VALIDATION: '1' - NEXT_TELEMETRY_DISABLED: '1' - -jobs: - quality: - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - steps: - - uses: actions/checkout@v4 - - - name: Install pnpm - run: npm install -g pnpm@9 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm -r run lint - continue-on-error: true # P1: ESLint 9 flat config 迁移未完成 - - - name: Typecheck - run: pnpm -r run typecheck - - - name: Test - run: pnpm -r run test - continue-on-error: true # P1: 部分服务无 test 脚本 - - - name: Build - run: pnpm -r run build - - arch-scan: - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - needs: quality - steps: - - uses: actions/checkout@v4 - - name: Install pnpm - run: npm install -g pnpm@9 - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: arch:scan - run: pnpm run arch:scan - - name: Verify arch.db committed - run: | - if git diff --quiet -- scripts/arch-scan/arch.db; then - echo "arch.db up to date" - else - echo "::warning::arch.db 未同步,请运行 pnpm run arch:scan 并提交" - fi - - docker-build: - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - needs: quality - strategy: - matrix: - service: - - { name: classes, dockerfile: services/classes/Dockerfile, context: . } - - { name: teacher-portal, dockerfile: apps/teacher-portal/Dockerfile, context: . } - steps: - - uses: actions/checkout@v4 - - name: Build ${{ matrix.service.name }} - run: | - docker build \ - -t edu/${{ matrix.service.name }}:ci \ - -f ${{ matrix.service.dockerfile }} \ - ${{ matrix.service.context }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ca79c02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,197 @@ +name: CI + +# CI/CD 一体化流水线(no-push 本地构建部署) +# 设计文档:docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md +# +# 流程: +# PR 触发:仅跑 3 个 quality job(并行) +# push main 触发:quality job 全绿后跑 deploy job(本地 build + compose up) +# workflow_dispatch:支持指定 commit_sha 回滚 +# +# 不依赖任何自建镜像,全部使用官方镜像 +# 不推送镜像到 registry,构建即部署 + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + commit_sha: + description: '回滚到指定 commit(留空则部署当前 HEAD)' + required: false + default: '' + +env: + SKIP_ENV_VALIDATION: '1' + NEXT_TELEMETRY_DISABLED: '1' + +jobs: + # ===== TypeScript 质量检查 ===== + quality-ts: + runs-on: ubuntu-latest + container: node:22-alpine + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha || github.ref }} + + - name: Install pnpm + run: npm install -g pnpm@9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm -r run lint + continue-on-error: true # P1: ESLint 9 flat config 迁移未完成 + + - name: Typecheck + run: pnpm -r run typecheck + + - name: Test + run: pnpm -r run test + continue-on-error: true # P1: 部分服务无 test 脚本 + + - name: Build + run: pnpm -r run build + + # ===== Go 质量检查 ===== + quality-go: + runs-on: ubuntu-latest + container: golang:1.22-alpine + defaults: + run: + working-directory: services/api-gateway + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha || github.ref }} + + - name: Download deps + run: go mod download + + - name: Vet + run: go vet ./... + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... -v -coverprofile=coverage.out + + # ===== Proto 契约检查 ===== + quality-proto: + runs-on: ubuntu-latest + container: bufbuild/buf:latest + defaults: + run: + working-directory: packages/shared-proto + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha || github.ref }} + fetch-depth: 0 # buf breaking 需要对比分支 + + - name: buf lint + run: buf lint + + - name: buf breaking + if: github.event_name == 'pull_request' + run: buf breaking --against .git#branch=main,subdir=packages/shared-proto + + # ===== 部署(仅 push main 或手动触发)===== + deploy: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + needs: [quality-ts, quality-go, quality-proto] + runs-on: ubuntu-latest + container: docker:25-git + options: --volume /var/run/docker.sock:/var/run/docker.sock + environment: + name: production + env: + DEPLOY_DIR: /opt/edu + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha || github.ref }} + + - name: Setup deploy dir + run: | + # 同步整个仓库到部署目录的 repo/ 子目录 + # compose 文件中 build.context 指向 ./repo/services/... 或 ./repo + mkdir -p ${{ env.DEPLOY_DIR }}/repo + cp -a $GITHUB_WORKSPACE/. ${{ env.DEPLOY_DIR }}/repo/ + cp infra/docker-compose.deploy.yml ${{ env.DEPLOY_DIR }}/docker-compose.yml + + # 确保 .env 存在(首次部署需人工创建) + if [ ! -f ${{ env.DEPLOY_DIR }}/.env ]; then + echo "::error::部署目录缺少 .env 文件,请参考 infra/deploy.env.example 创建" + exit 1 + fi + + - name: Deploy services (local build, no push) + run: | + cd ${{ env.DEPLOY_DIR }} + echo "=== 构建并启动服务 ===" + # --build 使用本地 Dockerfile 构建,不拉取 registry + # build.context 在 compose 中指向 ./repo/...(相对于 /opt/edu/) + docker compose up -d --build --remove-orphans + + - name: Wait for health + run: | + echo "等待服务健康..." + sleep 10 + + for i in 1 2 3 4 5 6 7 8 9 10; do + FAIL=0 + + echo "[尝试 $i] 检查 api-gateway..." + if ! wget -q --spider http://localhost:8080/healthz 2>/dev/null; then + echo " api-gateway 未就绪" + FAIL=1 + fi + + echo "[尝试 $i] 检查 classes..." + if ! wget -q --spider http://localhost:3001/healthz 2>/dev/null; then + echo " classes 未就绪" + FAIL=1 + fi + + echo "[尝试 $i] 检查 teacher-portal..." + if ! wget -q --spider http://localhost:3000/ 2>/dev/null; then + echo " teacher-portal 未就绪" + FAIL=1 + fi + + if [ $FAIL -eq 0 ]; then + echo "✅ 所有服务健康" + exit 0 + fi + + sleep 6 + done + + echo "::error::服务健康检查失败" + echo "=== 容器状态 ===" + cd ${{ env.DEPLOY_DIR }} && docker compose ps + echo "=== api-gateway 日志 ===" + docker compose logs --tail=50 api-gateway + echo "=== classes 日志 ===" + docker compose logs --tail=50 classes + echo "=== teacher-portal 日志 ===" + docker compose logs --tail=50 teacher-portal + exit 1 + + - name: Summary + if: always() + run: | + echo "### 部署结果" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 触发事件:\`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 部署 commit:\`${{ github.event.inputs.commit_sha || github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 部署目录:\`${{ env.DEPLOY_DIR }}\`" >> $GITHUB_STEP_SUMMARY + echo "- 部署方式:本地构建(no-push)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + cd ${{ env.DEPLOY_DIR }} && docker compose ps >> $GITHUB_STEP_SUMMARY 2>&1 || true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index c93fc0b..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: Deploy - -# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml -# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(job 内执行 docker compose) -# 部署到服务器 -# 触发条件:main 分支 docker.yml 完成后 自动,或手动 workflow_dispatch -# 部署方式:Runner 直接执行 docker compose(Runner 跑在服务器上) -# -# 前置条件(一次性配置): -# 1. 服务器上已安装 Gitea Actions Runner,标签为 'ubuntu-latest' -# 2. 服务器上已存在 /opt/edu/docker-compose.deploy.yml(由 SRE AI 首次部署) -# 3. 服务器上已存在 /opt/edu/.env(含生产 JWT_SECRET 等) -# 4. 服务器 Docker 已登录 Gitea Container Registry: -# docker login git.eazygame.cn -u -p -# 5. MySQL + Redis 已在服务器 Docker 中运行(3306/6379) -# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7 - -on: - workflow_dispatch: - inputs: - image_tag: - description: '镜像 tag(默认 latest)' - required: false - default: 'latest' - rollback: - description: '回滚到上一版本(跳过 pull)' - type: boolean - required: false - default: false - - workflow_run: - workflows: ['Docker Build & Push'] - types: [completed] - branches: [main] - -permissions: - contents: read - -env: - DEPLOY_DIR: /opt/edu - REGISTRY: git.eazygame.cn - IMAGE_TAG: ${{ github.event.inputs.image_tag || 'latest' }} - -jobs: - deploy: - # 仅在 docker.yml 成功后触发,或手动触发 - if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - environment: - name: production - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup deploy dir - run: | - sudo mkdir -p ${{ env.DEPLOY_DIR }} - sudo chown -R $(id -u):$(id -g) ${{ env.DEPLOY_DIR }} - - - name: Sync compose file - run: | - cp infra/docker-compose.deploy.yml ${{ env.DEPLOY_DIR }}/docker-compose.yml - # 确保 .env 存在(首次部署需人工创建) - if [ ! -f ${{ env.DEPLOY_DIR }}/.env ]; then - echo "::error::部署目录缺少 .env 文件,请参考 infra/deploy.env.example 创建" - exit 1 - fi - - - name: Login to Gitea Container Registry - run: | - # 使用预存的 ~/.docker/config.json(首次部署时人工 docker login) - # 或使用 secrets 注入 - if [ -n "${{ secrets.GITEA_REGISTRY_TOKEN }}" ]; then - echo "${{ secrets.GITEA_REGISTRY_TOKEN }}" | \ - docker login ${{ env.REGISTRY }} -u ${{ secrets.GITEA_REGISTRY_USER }} --password-stdin - fi - - - name: Pull images - if: ${{ github.event.inputs.rollback != 'true' }} - run: | - cd ${{ env.DEPLOY_DIR }} - export IMAGE_TAG=${{ env.IMAGE_TAG }} - docker compose pull - continue-on-error: true # 首次部署可能无旧镜像 - - - name: Deploy services - run: | - cd ${{ env.DEPLOY_DIR }} - export IMAGE_TAG=${{ env.IMAGE_TAG }} - docker compose up -d --remove-orphans - - - name: Wait for health - run: | - echo "等待服务健康..." - sleep 10 - - for i in 1 2 3 4 5 6 7 8 9 10; do - FAIL=0 - - echo "[尝试 $i] 检查 api-gateway..." - if ! curl -sf http://localhost:8080/healthz > /dev/null; then - echo " api-gateway 未就绪" - FAIL=1 - fi - - echo "[尝试 $i] 检查 classes..." - if ! curl -sf http://localhost:3001/healthz > /dev/null; then - echo " classes 未就绪" - FAIL=1 - fi - - echo "[尝试 $i] 检查 teacher-portal..." - if ! curl -sf http://localhost:3000/ > /dev/null; then - echo " teacher-portal 未就绪" - FAIL=1 - fi - - if [ $FAIL -eq 0 ]; then - echo "✅ 所有服务健康" - exit 0 - fi - - sleep 6 - done - - echo "::error::服务健康检查失败" - echo "=== 容器状态 ===" - cd ${{ env.DEPLOY_DIR }} && docker compose ps - echo "=== api-gateway 日志 ===" - docker compose logs --tail=50 api-gateway - echo "=== classes 日志 ===" - docker compose logs --tail=50 classes - exit 1 - - - name: Summary - if: always() - run: | - echo "### 部署结果" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- 镜像 tag:\`${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY - echo "- 部署目录:\`${{ env.DEPLOY_DIR }}\`" >> $GITHUB_STEP_SUMMARY - echo "- 回滚模式:${{ github.event.inputs.rollback || 'false' }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - cd ${{ env.DEPLOY_DIR }} && docker compose ps >> $GITHUB_STEP_SUMMARY 2>&1 || true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 60360b2..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Docker Build & Push - -# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml -# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(job 内执行 docker build/push) -# 触发条件:main 分支推送 或 打 v* tag -# 镜像推送到 Gitea Container Registry: git.eazygame.cn/xiner/edu/: -# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7 - -on: - push: - branches: [main] - paths: - - 'services/api-gateway/**' - - 'services/classes/**' - - 'apps/teacher-portal/**' - - 'packages/shared-proto/**' - - '.github/workflows/docker.yml' - tags: - - 'v*' - -permissions: - packages: write - contents: read - -env: - REGISTRY: git.eazygame.cn - OWNER: xiner - REPO_LOWER: edu - -jobs: - build-push: - runs-on: ubuntu-latest - container: dockerreg.eazygame.cn/node-with-docker:22 - strategy: - matrix: - service: - - { name: api-gateway, dockerfile: services/api-gateway/Dockerfile, context: ./services/api-gateway } - - { name: classes, dockerfile: services/classes/Dockerfile, context: . } - - { name: teacher-portal, dockerfile: apps/teacher-portal/Dockerfile, context: . } - steps: - - uses: actions/checkout@v4 - - # 登录 Gitea Container Registry - # GITHUB_TOKEN 由 Gitea Actions 自动注入,对应仓库写权限 - - name: Login to Gitea Container Registry - run: | - echo "${{ secrets.GITHUB_TOKEN }}" | \ - docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin - - # 生成镜像 tag - # main 分支:latest + git-sha - # v* tag:v + latest - - name: Extract metadata - id: meta - run: | - TAGS="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:latest" - SHA_TAG="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:sha-${GITHUB_SHA::7}" - TAGS="$TAGS,$SHA_TAG" - if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - VERSION="${GITHUB_REF#refs/tags/v}" - TAGS="$TAGS,${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:v${VERSION}" - fi - echo "tags=$TAGS" >> $GITHUB_OUTPUT - echo "Resolved tags: $TAGS" - - - name: Build and push ${{ matrix.service.name }} - run: | - IFS=',' read -ra TAG_ARRAY <<< "${{ steps.meta.outputs.tags }}" - TAG_ARGS="" - for tag in "${TAG_ARRAY[@]}"; do - TAG_ARGS="$TAG_ARGS -t $tag" - done - - docker build \ - $TAG_ARGS \ - -f ${{ matrix.service.dockerfile }} \ - ${{ matrix.service.context }} - - for tag in "${TAG_ARRAY[@]}"; do - echo "Pushing $tag..." - docker push $tag - done - - - name: Summary - run: | - echo "### Docker 镜像已推送" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "服务:\`${{ matrix.service.name }}\`" >> $GITHUB_STEP_SUMMARY - echo "镜像标签:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 527f050..b973675 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -455,77 +455,77 @@ services/[service]/src/ ## 15. CI/CD 规范 -> 详细配置见 `.github/workflows/`,本节为强制约束。 +> 详细配置见 `.github/workflows/ci.yml`,使用手册见 [cicd-runbook](../../docs/standards/cicd-runbook.md),设计文档见 [no-push-local-build-design](../../docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md)。 -### 15.1 流水线阶段(强制) +### 15.1 核心模式:no-push 本地构建 -所有 PR 与 main 分支推送必须通过以下阶段: +- **不推送镜像到 registry**:构建即部署,镜像只存在于构建机本地 +- **不依赖自建镜像**:全部使用官方镜像(node:22-alpine / golang:1.22-alpine / bufbuild/buf / docker:25-git) +- **DooD 模式**:deploy job 容器挂载 `/var/run/docker.sock`,容器内 docker 命令作用于宿主机 +- **单文件管理**:一个 `.github/workflows/ci.yml` 管全部 CI/CD -| 阶段 | 并行 | 内容 | 失败策略 | -| ---------------- | ------------ | ------------------------------------------ | -------- | -| **lint** | ✅(按语言) | TS ESLint / Go vet / Python ruff | 失败阻断 | -| **typecheck** | ✅ | TS `tsc --noEmit` | 失败阻断 | -| **test** | ✅(按服务) | Go test / Vitest / pytest | 失败阻断 | -| **build** | ✅(按服务) | Go build / nest build / next build | 失败阻断 | -| **arch:scan** | ✅ | `pnpm run arch:scan` + 校验 arch.db 一致性 | 失败阻断 | -| **docker-build** | ✅(按服务) | 构建镜像(不推送) | 失败阻断 | +### 15.2 流水线阶段 -> main 分支额外阶段:`docker-push`(推送镜像)+ `deploy`(部署到服务器) +| 阶段 | 并行 | 内容 | 失败策略 | +| ----------------- | ---- | ------------------------------------ | -------- | +| **quality-ts** | ✅ | pnpm lint + typecheck + test + build | 失败阻断 | +| **quality-go** | ✅ | go vet + build + test | 失败阻断 | +| **quality-proto** | ✅ | buf lint + buf breaking(仅 PR) | 失败阻断 | +| **deploy** | 串行 | docker compose up --build + 健康检查 | 失败阻断 | -### 15.2 触发条件 +> deploy job 仅在 `push main` 或 `workflow_dispatch` 时触发,PR 时不部署 -| 事件 | 触发阶段 | 触发条件 | -| ------------ | ---------------------------------------------------------- | ---------- | -| PR 创建/更新 | lint + typecheck + test + build + arch:scan + docker-build | 所有路径 | -| push 到 main | 上述全部 + docker-push + deploy | 合并后自动 | -| tag `v*` | docker-push(tag 镜像)+ deploy(生产) | 手动打 tag | +### 15.3 触发条件 -### 15.3 镜像规范 +| 事件 | 触发阶段 | 触发条件 | +| ----------------- | ----------------------------------------------- | -------------------------------- | +| PR 创建/更新 | quality-ts + quality-go + quality-proto(并行) | 所有路径 | +| push 到 main | 上述全部 + deploy | 合并后自动 | +| workflow_dispatch | 上述全部 + deploy | 手动触发,支持 `commit_sha` 回滚 | -- **镜像名**:`edu/:` -- **tag 策略**: - - `latest`:main 分支最新 - - ``:每次构建的 commit SHA(可追溯) - - `v`:正式发布 tag -- **镜像扫描**:docker-push 后自动运行 Trivy 扫描,CRITICAL 漏洞阻断部署 +> **不再支持 tag 发布**:no-push 模式下不用 `git tag v*` 触发。版本管理通过 commit SHA 追溯。 -### 15.4 部署策略 +### 15.4 镜像规范 + +- **不推送到 registry**,本地构建本地使用 +- **不保留历史镜像**:layer cache 在宿主机本地,未变更的层秒过 +- **回滚**:`git revert` 重跑 CI,或 `workflow_dispatch` 指定 `commit_sha` + +### 15.5 部署策略 - **目标环境**:服务器 Docker Compose(P1-P2 阶段),K8s(P3+ 阶段) -- **部署方式**:SSH 到服务器,`docker compose pull && docker compose up -d` -- **健康检查**:部署后轮询 `/healthz` 端点,连续 3 次失败回滚 -- **回滚**:`docker compose rollback`(上一版本镜像 tag) - -### 15.5 Secrets 管理 - -- **CI Secrets**:存于 GitHub Actions secrets / Gitea Actions secrets -- **部署服务器**:`.env` 文件由 SRE AI 管理,不进版本库 -- **JWT_SECRET**:生产环境强随机值,不与开发环境共享 +- **部署方式**:`docker compose up -d --build --remove-orphans`(在 `/opt/edu/` 目录) +- **部署目录**:`/opt/edu/`(compose 文件)+ `/opt/edu/repo/`(CI 同步的源码,供 build.context 使用) +- **健康检查**:部署后轮询 `/healthz` 端点(10 次 × 6 秒),失败输出容器日志 +- **回滚**:`git revert + push` 或 `workflow_dispatch` 指定 `commit_sha` ### 15.6 必需的 CI 文件 -| 文件 | 用途 | -| ------------------------------ | ------------------------------------------- | -| `.github/workflows/ci.yml` | 主流水线(lint + typecheck + test + build) | -| `.github/workflows/docker.yml` | Docker 镜像构建与推送(main + tag 触发) | -| `.github/workflows/deploy.yml` | 部署到服务器(main 触发,需环境审批) | +| 文件 | 用途 | +| --------------------------------- | ------------------------------------- | +| `.github/workflows/ci.yml` | 唯一 CI/CD 流水线(quality + deploy) | +| `infra/docker-compose.deploy.yml` | 部署用 compose(build: 替代 image:) | +| `infra/docker-compose.tools.yml` | 一次性预拉所有 CI 镜像 | +| `infra/deploy.env.example` | 部署环境变量模板 | -### 15.7 参考实现(强制参照) +### 15.7 actrunner 配置 -**所有 CI/CD 配置必须参照 `E:\Desktop\CICD\` 项目(同机 Gitea 仓库),不另行自创。** +```toml +# /etc/gitea/act_runner/config.yaml +container: + valid_volumes: + - /var/run/docker.sock +``` -- **参考项目位置**:`E:\Desktop\CICD\.gitea\workflows\ci.yml`(同机单仓库 CI 实现) -- **关键配置点(必须沿用)**: - - **Runner 标签**:`ubuntu-latest`(actrunner 已配置;参考项目用 `CDCD`,Edu 改为 `ubuntu-latest`) - - **容器化运行**:TS 类 job 必须使用 `container: dockerreg.eazygame.cn/node-with-docker:22`(带 docker 的 node 22 镜像,用于在 job 内执行 `docker build`/`docker run`) - - **Go 类 job**:使用 `container: golang:1.22-alpine`(仅 quality 阶段),`docker-build` 阶段切换到 `node-with-docker:22` 镜像 + `setup-go` - - **部署方式**:保留 `docker compose`(不照搬参考项目的 `docker run` 单容器方式,因 Edu 是微服务多容器) - - **代理配置**:runner 已配置全局代理,CI 内不再设置 npm/docker 代理(参考项目中的 `172.17.0.1:7890` 代理段不要照搬) - - **Next.js standalone 构建**:teacher-portal 沿用参考项目的 standalone 模式(`output: 'standalone'` + 复制 public/.next/static) - - **网络**:部署用 `edu-shared` 外部网络连接服务器已有 MySQL/Redis(参考项目用 `1panel-network`,Edu 不同) -- **改动需同步**:修改任何 workflow 文件前,先比对参考项目同类配置,确保不偏离上述约定 +### 15.8 镜像预拉(一次性) -> 当参考项目与本规范冲突时,以本规范为准(Edu 是微服务架构,参考项目是单体 Next.js)。 +部署前在服务器执行: + +```bash +docker compose -f infra/docker-compose.tools.yml pull +``` + +拉取清单:node:22-alpine、golang:1.22-alpine、bufbuild/buf:latest、docker:25-git、node:20-alpine、alpine:3.20、mysql:8.0(开发测试)、redis:7-alpine(开发测试)。 --- diff --git a/docs/standards/cicd-runbook.md b/docs/standards/cicd-runbook.md index c446637..6f67a6f 100644 --- a/docs/standards/cicd-runbook.md +++ b/docs/standards/cicd-runbook.md @@ -1,99 +1,113 @@ # CI/CD 使用手册(CI/CD Runbook) -> 版本:1.0 +> 版本:2.0(no-push 本地构建模式) > 日期:2026-07-08 -> 适用范围:Edu 微服务项目(Gitea Actions + Gitea Container Registry + Runner 直接部署) +> 适用范围:Edu 微服务项目(Gitea Actions + Runner 本地构建部署) > 关联文档:[project_rules §15](../../.trae/rules/project_rules.md)、[本地启动手册](./local-dev-runbook.md)、[多 AI 协作指南](./multi-ai-collaboration.md) +> 设计文档:[2026-07-08-cicd-no-push-local-build-design.md](../superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md) --- ## 1. 架构总览 ``` -开发 AI 推送 PR 协调 AI 合并到 main +PR 触发(开发 AI 提 PR) push main 触发(协调 AI 合并) │ │ ▼ ▼ -┌─────────────┐ ┌─────────────┐ -│ ci-*.yml │ │ docker.yml │ -│ lint+test+ │ │ build+push │ -│ build+scan │ │ 镜像到 Gitea│ -└─────────────┘ │ Registry │ - └──────┬──────┘ - │ - ▼ - ┌─────────────┐ - │ deploy.yml │ - │ Runner 直接 │ - │ docker │ - │ compose up │ - └─────────────┘ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ ci.yml │ │ ci.yml │ +│ quality-ts (node:22) │ │ quality-ts (node:22) │ +│ quality-go (golang) │ │ quality-go (golang) │ +│ quality-proto (buf) │ │ quality-proto (buf) │ +│ (并行,不部署) │ │ │ │ +└─────────────────────────┘ │ ▼ │ + │ deploy job │ + │ (docker:25-git) │ + │ 挂载 /var/run/docker.sock│ + │ │ │ + │ ▼ │ + │ docker compose up │ + │ --build(本地构建) │ + │ │ │ + │ ▼ │ + │ 健康检查轮询 │ + └─────────────────────────┘ ``` -### 1.1 组件清单 +### 1.1 核心特点 -| 组件 | 说明 | -| ---------------------------- | ---------------------------------------------------------- | -| **Gitea** | `git.eazygame.cn`,代码托管 + Actions + Container Registry | -| **Gitea Actions** | CI 运行器,兼容 GitHub Actions 语法 | -| **Gitea Container Registry** | 镜像仓库,地址 `git.eazygame.cn/xiner/edu/:` | -| **Deploy Runner** | 跑在服务器上的 actrunner,标签 `ubuntu-latest` | -| **服务器 MySQL** | 已有容器,端口 3306,容器名 `edu-mysql`(假设) | -| **服务器 Redis** | 已有容器,端口 6379,容器名 `edu-redis`(假设) | +- **单文件管理**:一个 `.github/workflows/ci.yml` 管全部 CI/CD +- **no-push 本地构建**:不推送到 registry,构建即部署 +- **不依赖自建镜像**:全部使用官方镜像(node/golang/buf/docker) +- **DooD 模式**:deploy job 容器内挂载宿主机 `/var/run/docker.sock` -### 1.2 流水线文件 +### 1.2 组件清单 -| 文件 | 触发 | 作用 | -| -------------------------------- | ------------------------ | ---------------------------------------------------------- | -| `.github/workflows/ci-ts.yml` | TS 路径变更 | lint + typecheck + test + build + arch:scan + docker-build | -| `.github/workflows/ci-go.yml` | Go 路径变更 | vet + build + test + docker-build | -| `.github/workflows/ci-py.yml` | Python 路径变更 | ruff + pytest(P4 阶段) | -| `.github/workflows/ci-proto.yml` | proto 变更 | buf lint + buf breaking | -| `.github/workflows/docker.yml` | push main / tag `v*` | 构建并推送镜像到 Gitea Registry | -| `.github/workflows/deploy.yml` | docker.yml 完成后 / 手动 | Runner 执行 docker compose up | +| 组件 | 说明 | +| ----------------- | ------------------------------------------- | +| **Gitea** | `git.eazygame.cn`,代码托管 + Actions | +| **Gitea Actions** | CI 运行器,兼容 GitHub Actions 语法 | +| **actrunner** | 跑在服务器上的 runner,标签 `ubuntu-latest` | +| **服务器 MySQL** | 已有容器,端口 3306 | +| **服务器 Redis** | 已有容器,端口 6379 | + +### 1.3 流水线文件 + +| 文件 | 触发 | 作用 | +| --------------------------------- | ---------------------------------- | --------------------------------------------- | +| `.github/workflows/ci.yml` | PR + push main + workflow_dispatch | quality(3 job 并行)+ deploy(仅 push main) | +| `infra/docker-compose.deploy.yml` | deploy job 调用 | 多服务编排(build: 替代 image:) | +| `infra/docker-compose.tools.yml` | 手动执行 | 一次性预拉所有 CI 镜像 | --- -## 2. 一次性配置(SRE AI 或人类执行) +## 2. 一次性配置 -### 2.1 启用 Gitea Actions +### 2.1 启用 Gitea Actions + actrunner -在 Gitea 仓库 `xiner/Edu` 设置: +1. 仓库设置 → Actions → 启用 +2. 安装 actrunner 并注册(标签 `ubuntu-latest`) +3. 配置 actrunner 允许挂载 docker.sock: -1. **仓库设置 → Actions → 启用** -2. 安装 actrunner(若未安装): - ```bash - # 在服务器上 - wget https://gitea.com/gitea/act_runner/raw/branch/main/act_runner - chmod +x act_runner - ./act_runner register --instance https://git.eazygame.cn --token - ``` -3. 配置 runner 标签为 `ubuntu-latest`(CI 与部署共用) - -### 2.2 启用 Gitea Container Registry - -Gitea 1.20+ 自带 Container Registry,无需额外启用。确认: - -```bash -# 测试登录 -docker login git.eazygame.cn -u <你的用户名> -# 输入密码或 token +```toml +# /etc/gitea/act_runner/config.yaml +container: + valid_volumes: + - /var/run/docker.sock ``` -### 2.3 准备服务器网络 +### 2.2 预拉所有 CI 镜像(一次性) -确保 MySQL/Redis 容器与应用容器在同一 Docker 网络: +```bash +# 在服务器上,进入 Edu 仓库目录 +cd /path/to/Edu +docker compose -f infra/docker-compose.tools.yml pull +``` + +拉取的镜像清单: + +| 镜像 | 用途 | +| --------------------- | --------------------------------------------------- | +| `node:22-alpine` | CI quality-ts job | +| `golang:1.22-alpine` | CI quality-go job + api-gateway builder | +| `bufbuild/buf:latest` | CI quality-proto job | +| `docker:25-git` | CI deploy job(自带 docker CLI + compose v2 + git) | +| `node:20-alpine` | teacher-portal / classes builder | +| `alpine:3.20` | api-gateway runner | +| `mysql:8.0` | 开发测试用(生产用已有的) | +| `redis:7-alpine` | 开发测试用(生产用已有的) | + +### 2.3 准备服务器网络 ```bash # 创建共享网络(若不存在) docker network create edu-shared # 将已有的 MySQL/Redis 加入网络(容器名按实际替换) -docker network connect edu-shared edu-mysql # 或实际容器名 -docker network connect edu-shared edu-redis # 或实际容器名 +docker network connect edu-shared <实际mysql容器名> +docker network connect edu-shared <实际redis容器名> ``` -> 查看实际容器名:`docker ps --format "{{.Names}}" | grep -E "mysql|redis"` - ### 2.4 初始化部署目录 ```bash @@ -101,35 +115,16 @@ docker network connect edu-shared edu-redis # 或实际容器名 sudo mkdir -p /opt/edu sudo chown -R $USER:$USER /opt/edu -# 2. 拷贝 compose 文件 -cd /path/to/Edu -cp infra/docker-compose.deploy.yml /opt/edu/docker-compose.yml - -# 3. 创建生产 .env(从模板) +# 2. 创建生产 .env(从模板) cp infra/deploy.env.example /opt/edu/.env vim /opt/edu/.env # 必须修改: # JWT_SECRET= # DATABASE_URL=mysql://:@:3306/ # REDIS_URL=redis://:6379 - -# 4. 登录 Gitea Container Registry(Runner 用户) -docker login git.eazygame.cn -u -p -# token 在 Gitea → 设置 → 应用 → 生成新 token ``` -### 2.5 配置 Gitea Secrets - -在 Gitea 仓库 → 设置 → Actions → Secrets,添加: - -| Secret 名 | 用途 | 必填 | -| ---------------------- | -------------------------------- | ---- | -| `GITHUB_TOKEN` | Gitea 自动注入,推送镜像用 | 自动 | -| `GITEA_REGISTRY_USER` | deploy.yml 登录 registry(可选) | 否 | -| `GITEA_REGISTRY_TOKEN` | deploy.yml 登录 registry(可选) | 否 | - -> `GITHUB_TOKEN` 由 Gitea Actions 自动注入,无需手动配置。`docker.yml` 用它推送镜像。 -> `deploy.yml` 中 Runner 已在服务器上,若已 `docker login` 过则无需 secrets。 +> 注意:`/opt/edu/repo/` 子目录由 CI 自动同步,不需要手动准备。 --- @@ -137,117 +132,71 @@ docker login git.eazygame.cn -u -p ### 3.1 开发 AI 提 PR -开发 AI 推送特性分支后创建 PR,自动触发 CI: - ``` -PR 创建 → ci-ts.yml / ci-go.yml 运行 - ├─ quality(lint + typecheck + test + build) - ├─ arch-scan(架构扫描) - └─ docker-build(构建不推送) +PR 创建 → ci.yml 运行 + ├─ quality-ts(pnpm lint + typecheck + test + build) + ├─ quality-go(go vet + build + test) + └─ quality-proto(buf lint + buf breaking) + (3 个 job 并行,仅 quality,不部署) CI 全绿 → 协调 AI 审核合并 ``` ### 3.2 协调 AI 合并 PR -PR 合并到 main 后,自动触发: +PR 合并到 main 后自动触发: ``` -push main → - ├─ ci-*.yml(确保 main 稳定) - └─ docker.yml → 构建并推送镜像到 git.eazygame.cn/xiner/edu/:latest +push main → ci.yml + ├─ quality-* (3 个 job 并行,确保 main 稳定) + └─ deploy job(needs: [quality-ts, quality-go, quality-proto]) │ ▼ - deploy.yml → Runner 执行 docker compose pull && up -d + 同步代码到 /opt/edu/repo/ │ ▼ - 健康检查(轮询 /healthz) + docker compose up -d --build(本地构建 3 个服务) + │ + ▼ + 健康检查轮询(10 次 × 6 秒) ``` -### 3.3 手动部署 +### 3.3 手动触发部署 -在 Gitea → Actions → Deploy → Run workflow: +在 Gitea → Actions → CI → Run workflow: -| 参数 | 说明 | -| ----------- | --------------------------------------- | -| `image_tag` | 部署指定 tag(默认 `latest`) | -| `rollback` | 勾选则跳过 pull,用本地已有镜像重新启动 | +- `commit_sha`(可选):回滚到指定 commit,留空则部署当前 HEAD -### 3.4 发布正式版本(tag) +### 3.4 不再支持 tag 发布 -```bash -git tag -a v1.0.0 -m "P1 阶段首次发布" -git push origin v1.0.0 -``` - -触发 `docker.yml` 推送 `v1.0.0` tag 镜像,再手动触发 deploy.yml 部署 `v1.0.0`。 +no-push 模式下,镜像不存放到 registry,因此不再使用 `git tag v*` 触发发布。版本管理通过 git commit SHA 追溯。 --- -## 4. 镜像管理 +## 4. 部署验证 -### 4.1 镜像地址 +### 4.1 CI 自动验证 -``` -git.eazygame.cn/xiner/edu/: -``` - -| service | 说明 | -| ---------------- | --------------- | -| `api-gateway` | Go 网关 | -| `classes` | NestJS 班级服务 | -| `teacher-portal` | Next.js 前端 | - -### 4.2 Tag 策略 - -| tag | 来源 | 用途 | -| ------------------ | ----------------- | ---------- | -| `latest` | main 分支最新构建 | 默认部署 | -| `main-` | main 分支每次构建 | 可追溯 | -| `v` | 打 `v*` tag 触发 | 正式版本 | -| `v.` | 打 `v*` tag 触发 | 大版本追踪 | - -### 4.3 查看与清理镜像 - -在 Gitea → 用户设置 → Packages,可查看与管理所有镜像。 - -清理旧 tag(手动): - -```bash -# 列出所有 tag -docker images git.eazygame.cn/xiner/edu/classes --format "{{.Tag}}" - -# 删除本地旧镜像 -docker rmi git.eazygame.cn/xiner/edu/classes:main-abc1234 -``` - ---- - -## 5. 部署验证 - -### 5.1 CI 自动验证 - -`deploy.yml` 部署后会自动轮询健康检查(最多 10 次,每次间隔 6 秒): +deploy job 部署后会自动轮询健康检查: - `http://localhost:8080/healthz` — api-gateway - `http://localhost:3001/healthz` — classes - `http://localhost:3000/` — teacher-portal -失败时输出容器日志,方便排查。 +失败时输出容器状态与日志,方便排查。 -### 5.2 手动验证 +### 4.2 手动验证 ```bash -# SSH 到服务器后 cd /opt/edu # 容器状态 docker compose ps # 健康检查 -curl http://localhost:8080/healthz # api-gateway -curl http://localhost:3001/healthz # classes -curl http://localhost:3000/ # teacher-portal +curl http://localhost:8080/healthz +curl http://localhost:3001/healthz +curl http://localhost:3000/ # 鉴权验证(生产模式 dev-token 应被拒绝) curl -H "Authorization: Bearer dev-token" http://localhost:8080/api/v1/classes @@ -259,40 +208,11 @@ docker compose logs -f classes docker compose logs -f teacher-portal ``` -### 5.3 外部访问 - -若服务器有公网 IP(如 `1.2.3.4`): - -- 前端:`http://1.2.3.4:3000` -- API 网关:`http://1.2.3.4:8080` -- 健康检查:`http://1.2.3.4:8080/healthz` - --- -## 6. 回滚 +## 5. 回滚 -### 6.1 CI 回滚(推荐) - -在 Gitea → Actions → Deploy → Run workflow: - -- `image_tag`:填入上一个稳定版本的 tag(如 `main-abc1234`) -- `rollback`:勾选(跳过 pull,用本地已有镜像) - -### 6.2 手动回滚 - -```bash -cd /opt/edu - -# 查看本地镜像 -docker images git.eazygame.cn/xiner/edu/classes --format "{{.Tag}}" - -# 回滚到指定版本 -IMAGE_TAG=main-abc1234 docker compose up -d -``` - -### 6.3 紧急回滚(Git revert) - -若代码有问题: +### 5.1 git revert(推荐) ```bash git revert @@ -300,54 +220,62 @@ git push origin main # CI 自动重新构建部署 ``` +### 5.2 手动触发指定 commit + +在 Gitea → Actions → CI → Run workflow: + +- `commit_sha`:填入上一个稳定版本的 commit SHA + +CI 会 checkout 指定 commit → 本地重新 build → deploy。 + +### 5.3 不再支持镜像 tag 回滚 + +no-push 模式下没有 registry tag,不能像旧方案那样切换 `IMAGE_TAG`。回滚必须重新构建。 + --- -## 7. 常见问题 +## 6. 常见问题 -### 7.1 CI: pnpm install 失败 +### 6.1 CI: pnpm install 失败 **症状**:`pnpm install --frozen-lockfile` 报错。 **排查**: -1. `pnpm-lock.yaml` 未提交:`git add pnpm-lock.yaml && git commit -m "chore(deps): 更新 lockfile"` -2. Node 版本不匹配:确认 CI 用 Node 20,本地一致 +1. `pnpm-lock.yaml` 未提交:`git add pnpm-lock.yaml && git commit` +2. Node 版本不匹配:确认 CI 用 Node 22,本地一致 -### 7.2 CI: docker-build 失败 +### 6.2 CI: deploy job 无法访问 docker -**症状**:`docker/build-push-action` 报错。 +**症状**:`docker compose up` 报 `Cannot connect to the Docker daemon`。 -**排查**: - -1. Dockerfile 语法错误:本地 `docker build` 验证 -2. 路径错误:确认 `context` 与 `file` 参数 - -### 7.3 CD: 镜像拉取失败 - -**症状**:`docker compose pull` 报 `unauthorized`。 +**原因**:actrunner 没有挂载 `/var/run/docker.sock`,或 `valid_volumes` 未配置。 **修复**: -```bash -# 服务器上重新登录 -docker login git.eazygame.cn -u -p - -# 或配置 Gitea secrets GITEA_REGISTRY_USER / GITEA_REGISTRY_TOKEN +```toml +# /etc/gitea/act_runner/config.yaml +container: + valid_volumes: + - /var/run/docker.sock ``` -### 7.4 CD: 健康检查失败 +重启 actrunner 后重试。 -**症状**:`deploy.yml` 健康检查 10 次后失败。 +### 6.3 CD: 健康检查失败 + +**症状**:deploy job 健康检查 10 次后失败。 **排查**: -1. 查看日志:`cd /opt/edu && docker compose logs api-gateway` +1. CI 输出会自动打印容器状态和日志 2. 常见原因: - `DATABASE_URL` 连不上 MySQL(检查容器名与网络) - `JWT_SECRET` 未配置 - 端口被占用:`docker ps` 检查冲突 + - `/opt/edu/.env` 不存在或格式错误 -### 7.5 CD: 连不上 MySQL/Redis +### 6.4 CD: 连不上 MySQL/Redis **症状**:classes 容器报 `ECONNREFUSED edu-mysql:3306`。 @@ -367,31 +295,18 @@ docker network connect edu-shared <实际mysql容器名> # DATABASE_URL=mysql://edu:changeme@<实际容器名>:3306/next_edu_cloud ``` -### 7.6 Gitea Actions 未触发 - -**症状**:push 后 Actions 不运行。 +### 6.5 Gitea Actions 未触发 **排查**: 1. 仓库设置 → Actions → 确认已启用 2. Runner 在线:Gitea → 设置 → Actions → Runners -3. workflow 文件在 `.github/workflows/` 目录(非 `.gitea/workflows/`) -4. `on:` 触发条件匹配(paths 过滤) - -### 7.7 workflow_run 不触发 - -**症状**:`docker.yml` 完成后 `deploy.yml` 不自动运行。 - -**原因**:Gitea Actions 对 `workflow_run` 触发支持可能不完整。 - -**解决**: - -- 改用手动触发:Actions → Deploy → Run workflow -- 或在 `docker.yml` 末尾加 job 调用 deploy(需要 `workflow_call`) +3. workflow 文件在 `.github/workflows/` 目录 +4. `on:` 触发条件匹配 --- -## 8. 排查命令速查 +## 7. 排查命令速查 ```bash # === 在服务器上 === @@ -405,6 +320,12 @@ docker compose -f /opt/edu/docker-compose.yml logs -f # 重启单个服务 docker compose -f /opt/edu/docker-compose.yml restart api-gateway +# 重新构建并启动(手动触发部署) +cd /opt/edu/repo +git pull +cd /opt/edu +docker compose up -d --build + # 进入容器 docker exec -it edu-api-gateway sh docker exec -it edu-classes sh @@ -412,95 +333,31 @@ docker exec -it edu-classes sh # 查看网络 docker network inspect edu-shared -# 查看镜像列表 -docker images git.eazygame.cn/xiner/edu +# 查看本地镜像 +docker images | grep -E "node|golang|docker|alpine|buf" # === 在 Gitea Web UI === # 仓库 → Actions → 查看流水线运行记录 -# 用户设置 → Packages → 管理镜像 -# 仓库 → 设置 → Secrets → 管理 CI 密钥 # 仓库 → 设置 → Actions → Runners → 查看 Runner 状态 ``` --- -## 9. 安全注意事项 +## 8. 安全注意事项 1. **`.env` 文件不入库**:`.gitignore` 已忽略,仅存在于服务器 `/opt/edu/.env` 2. **JWT_SECRET 强随机**:生产必须用 `openssl rand -hex 32` 生成 3. **DEV_MODE=false**:docker-compose.deploy.yml 强制设为 `false` -4. **Gitea Token 权限最小化**:仅给 `package:write` 和 `contents:read` -5. **Runner 隔离**:deploy runner 仅跑在目标服务器,不暴露公网 SSH -6. **镜像扫描**(P6):`docker.yml` 后续加 Trivy 扫描步骤 +4. **docker.sock 安全**:actrunner 仅在部署服务器运行,已隔离 +5. **Runner 隔离**:runner 跑在服务器上,不暴露公网 SSH --- -## 10. 参考实现(强制参照) - -**所有 CI/CD 配置变更必须参照同机的 `E:\Desktop\CICD\` 项目(Gitea 单仓库 CI 实现),不另行自创。** - -### 10.1 参考项目位置 - -| 路径 | 内容 | -| ------------------------------------------------- | ------------------------------------- | -| `E:\Desktop\CICD\.gitea\workflows\ci.yml` | 主 CI 配置(build-deploy + 安全扫描) | -| `E:\Desktop\CICD\.gitea\workflows\dr-drill.yml` | 灾备演练 | -| `E:\Desktop\CICD\.gitea\workflows\lighthouse.yml` | 性能审计 | -| `E:\Desktop\CICD\.gitea\workflows\security.yml` | 安全扫描 | -| `E:\Desktop\CICD\.trae\rules\project_rules.md` | 参考项目规则(对照学习) | - -### 10.2 关键配置点(必须沿用) - -参考项目 `ci.yml` 的核心配置: - -| 配置项 | 参考项目值 | Edu 项目取值 | -| ------------------- | ------------------------------------------------------ | ------------------------------------------- | -| **Runner 标签** | `CDCD` | `ubuntu-latest` | -| **TS job 容器** | `dockerreg.eazygame.cn/node-with-docker:22` | 同(带 docker 的 node 22 镜像) | -| **Go quality 容器** | (无 Go) | `golang:1.22-alpine` | -| **Go docker-build** | (无 Go) | `node-with-docker:22` + `setup-go` | -| **部署方式** | `docker build` + `docker run --network 1panel-network` | `docker compose pull && up`(多服务编排) | -| **网络** | `1panel-network` | `edu-shared`(连接已有 MySQL/Redis) | -| **npm 代理** | docker gateway IP `172.17.0.1:7890` | 不配置(runner 已全局代理) | -| **Next.js 构建** | standalone + 复制 `public`/`.next/static` | teacher-portal 沿用 | -| **环境变量** | secrets 注入 `DATABASE_URL`/`NEXTAUTH_SECRET` | secrets 注入 `DATABASE_URL`/`JWT_SECRET` 等 | -| **定时备份** | `schedule: cron "0 2 * * *"` | 待 P6 阶段实现 | - -### 10.3 关键差异(Edu vs 参考项目) - -| 维度 | 参考项目 | Edu 项目 | -| -------- | ------------------------ | ------------------------------------------------------ | -| 架构 | 单体 Next.js | 微服务(TS + Go + Python + Proto) | -| 包管理器 | npm | pnpm(TS)/ go mod(Go)/ uv(Python) | -| 服务数量 | 1 个(nextjs-app) | 3+(api-gateway / classes / teacher-portal) | -| 部署方式 | docker run 单容器 | docker compose 多服务编排 | -| CI 拆分 | 单 ci.yml 大而全 | 按语言拆 ci-ts/ci-go/ci-py/ci-proto | -| 镜像仓库 | 本地 `nextjs-app:latest` | Gitea Registry `git.eazygame.cn/xiner/edu/:` | - -### 10.4 修改 workflow 的流程 - -1. **先读参考项目同类配置**:`Read E:\Desktop\CICD\.gitea\workflows\ci.yml` -2. **比对差异**:参照 §10.3 表格,确认 Edu 项目的特殊取值 -3. **修改 Edu workflow**:保持关键配置点(container、runner 标签)一致 -4. **YAML 语法校验**:`python -c "import yaml; yaml.safe_load(open(''))"` -5. **提交并推送**:触发 CI 验证 - -### 10.5 当参考项目与本规范冲突时 - -- **架构差异**(单体 vs 微服务):以 Edu 项目规范为准 -- **部署方式**(docker run vs compose):以 Edu 项目规范为准 -- **镜像/网络/标签**:以 Edu 项目规范为准 -- **CI 容器化、构建流程、缓存策略**:参照参考项目 - -> 详见 [project_rules §15.7 参考实现](../../.trae/rules/project_rules.md) - ---- - -## 11. 相关文档 +## 9. 相关文档 - [project_rules §15 CI/CD 规范](../../.trae/rules/project_rules.md) - [project_rules §14 多 AI 协作规范](../../.trae/rules/project_rules.md) - [本地启动手册](./local-dev-runbook.md) - [多 AI 协作指南](./multi-ai-collaboration.md) -- [Git 工作流](./git-workflow.md) - [known-issues](../troubleshooting/known-issues.md) +- [CI/CD 设计文档](../superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md) diff --git a/docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md b/docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md new file mode 100644 index 0000000..81b2d1d --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md @@ -0,0 +1,137 @@ +# CI/CD 设计:本地构建部署(no-push) + +> 日期:2026-07-08 +> 状态:已批准,待实施 +> 替代方案:[2026-07-08 之前的 docker.yml + deploy.yml push 方案](../../.github/workflows/) + +## 1. 背景 + +### 1.1 现状问题 + +- `node-with-docker:22` 自定义镜像已丢失,原方案依赖它无法运行 +- 推送到 Gitea Registry 的拉取速度极慢,影响部署效率 +- 用户习惯本地构建测试后再推送,无需保留历史构建产物 +- 微服务架构下,拆分多个 workflow 文件(ci-ts/ci-go/docker/deploy)使流程碎片化 + +### 1.2 目标 + +- 不依赖任何自建镜像,全部使用官方镜像 +- 不推送镜像到 registry,构建即部署 +- 单文件管理整个 CI/CD 流程 +- 支持回滚(不依赖 registry tag) +- 提供一次性预拉所有镜像的 compose 文件 + +## 2. 设计 + +### 2.1 架构总览 + +``` +PR/push 触发 + │ + ├─ quality-ts job (container: node:22-alpine) 并行 + ├─ quality-go job (container: golang:1.22-alpine) 并行 + ├─ quality-proto job (container: bufbuild/buf:latest) 并行 + │ + └─ deploy job (container: docker:25-git) 仅 push main, needs: [quality-*] + 挂载 /var/run/docker.sock(DooD) + ├─ docker compose up --build(本地构建 3 个服务) + └─ 健康检查轮询 +``` + +### 2.2 文件结构 + +``` +.github/workflows/ +└─ ci.yml # 唯一的 CI/CD 文件 + +infra/ +├─ docker-compose.deploy.yml # 改造:build: 替代 image: +├─ docker-compose.tools.yml # 新建:预拉所有 CI 需要的镜像 +└─ deploy.env.example # 保留 +``` + +### 2.3 ci.yml 设计 + +**单文件多 job**: + +- `quality-ts`:PR+push 都跑,container: node:22-alpine,pnpm install → lint → typecheck → test → build +- `quality-go`:PR+push 都跑,container: golang:1.22-alpine,go mod download → vet → build → test +- `quality-proto`:PR+push 都跑,container: bufbuild/buf:latest,buf lint + buf breaking(仅 PR) +- `deploy`:仅 push main 或 workflow_dispatch 触发,needs: [quality-ts, quality-go, quality-proto] + +**deploy job 关键配置**: + +- `container: docker:25-git`(官方镜像,自带 docker CLI + compose v2 + git) +- `options: --volume /var/run/docker.sock:/var/run/docker.sock`(DooD 模式) +- 流程:checkout → cp compose 文件到 /opt/edu/ → docker compose up --build → 健康检查 + +**回滚**: + +- `workflow_dispatch` 支持 `commit_sha` 输入 +- checkout 时使用指定 commit SHA +- 重新 build + deploy + +### 2.4 docker-compose.deploy.yml 改造 + +所有服务从 `image:` 改为 `build:`: + +- `api-gateway`:`build: ./services/api-gateway` +- `classes`:`build: { context: ., dockerfile: services/classes/Dockerfile }`(monorepo 上下文) +- `teacher-portal`:`build: { context: ., dockerfile: apps/teacher-portal/Dockerfile }`(monorepo 上下文) + +删除 `IMAGE_TAG` 环境变量依赖。`docker compose up --build` 自动判断变更的服务。 + +### 2.5 docker-compose.tools.yml(预拉镜像) + +用于一次性拉取所有 CI/构建需要的镜像到本地: + +- CI 运行时:node:22-alpine、golang:1.22-alpine、bufbuild/buf:latest、docker:25-git +- 服务构建基础:node:20-alpine、golang:1.22-alpine、alpine:3.20 +- 开发基础设施:mysql:8.0、redis:7-alpine + +用法:`docker compose -f infra/docker-compose.tools.yml pull` + +### 2.6 回滚策略 + +| 方式 | 操作 | 适用场景 | +| ---------- | --------------------------------------------------- | ---------------- | +| git revert | `git revert && git push` → 自动触发 CI | 代码回滚(推荐) | +| 手动触发 | Actions → ci.yml → Run workflow → 填 commit_sha | 部署特定版本 | + +### 2.7 actrunner 配置 + +```toml +container: + valid_volumes: + - /var/run/docker.sock +``` + +## 3. 实施步骤 + +1. 删除现有 4 个 workflow 文件(ci-ts.yml、ci-go.yml、ci-proto.yml、docker.yml、deploy.yml) +2. 创建 `.github/workflows/ci.yml`(合并单 workflow) +3. 改造 `infra/docker-compose.deploy.yml`(build 替代 image) +4. 创建 `infra/docker-compose.tools.yml`(预拉镜像) +5. 更新 `.trae/rules/project_rules.md` §15(no-push 模式规范) +6. 更新 `docs/standards/cicd-runbook.md`(删除 registry 章节,新增本地构建章节) +7. 提交并推送 + +## 4. 与旧方案对比 + +| 维度 | 旧方案(push 到 registry) | 新方案(no-push 本地构建) | +| --------------- | -------------------------- | ------------------------------ | +| workflow 文件数 | 4 个 | 1 个 | +| 镜像推送 | push 到 Gitea Registry | 不推送 | +| 部署方式 | compose pull + up | compose up --build | +| 自建镜像 | 依赖 node-with-docker:22 | 全用官方镜像 | +| 回滚 | 切换 registry tag | git revert / workflow_dispatch | +| 构建产物保留 | registry 保留历史 | 不保留 | + +## 5. 风险与缓解 + +| 风险 | 缓解 | +| ------------------------- | ---------------------------------------------- | +| 本地镜像被清理后无法回滚 | 回滚走 git revert + 重新 build,不依赖镜像缓存 | +| docker.sock 挂载安全风险 | actrunner 仅在部署服务器运行,已隔离 | +| 构建慢 | layer cache 在宿主机本地,未变更的层秒过 | +| Gitea workflow_run 不支持 | 用 needs 串联,不用 workflow_run | diff --git a/infra/docker-compose.deploy.yml b/infra/docker-compose.deploy.yml index 0a23594..da216c9 100644 --- a/infra/docker-compose.deploy.yml +++ b/infra/docker-compose.deploy.yml @@ -1,23 +1,27 @@ -# 服务器部署用 Docker Compose -# 镜像来源:Gitea Container Registry (git.eazygame.cn/xiner/edu/:) +# 服务器部署用 Docker Compose(no-push 本地构建模式) +# 镜像来源:CI 容器内本地 docker build(不推送到 registry) # 基础设施:MySQL + Redis 已在服务器 Docker 中运行(不在此文件管理) # # 部署目录:/opt/edu/ # 部署命令(CI 自动执行): -# IMAGE_TAG=latest docker compose pull && docker compose up -d +# docker compose up -d --build --remove-orphans # # 首次部署手动步骤: # 1. sudo mkdir -p /opt/edu && sudo chown -R $USER:$USER /opt/edu # 2. cp infra/docker-compose.deploy.yml /opt/edu/docker-compose.yml # 3. cp infra/deploy.env.example /opt/edu/.env && 编辑填入生产密钥 -# 4. docker login git.eazygame.cn -u -p -# 5. cd /opt/edu && IMAGE_TAG=latest docker compose up -d +# 4. docker compose up -d --build +# +# 注意:compose 文件中的 build.context 路径相对于 /opt/edu/ 目录 +# CI 在 deploy 步骤会先把仓库 checkout 到 /opt/edu/repo/,再 cp compose 文件到 /opt/edu/ name: edu services: api-gateway: - image: git.eazygame.cn/xiner/edu/api-gateway:${IMAGE_TAG:-latest} + build: + context: ./repo/services/api-gateway + dockerfile: Dockerfile container_name: edu-api-gateway restart: unless-stopped environment: @@ -48,7 +52,9 @@ services: - edu-shared classes: - image: git.eazygame.cn/xiner/edu/classes:${IMAGE_TAG:-latest} + build: + context: ./repo + dockerfile: services/classes/Dockerfile container_name: edu-classes restart: unless-stopped environment: @@ -70,7 +76,9 @@ services: - edu-shared teacher-portal: - image: git.eazygame.cn/xiner/edu/teacher-portal:${IMAGE_TAG:-latest} + build: + context: ./repo + dockerfile: apps/teacher-portal/Dockerfile container_name: edu-teacher-portal restart: unless-stopped environment: diff --git a/infra/docker-compose.tools.yml b/infra/docker-compose.tools.yml new file mode 100644 index 0000000..c6addd5 --- /dev/null +++ b/infra/docker-compose.tools.yml @@ -0,0 +1,55 @@ +# CI/CD 工具镜像预拉(一次性拉取所有 CI 与构建需要的镜像到本地) +# 用途:actrunner 的 container job 会优先使用本地镜像缓存,避免每次 CI 联网拉取 +# +# 用法: +# docker compose -f infra/docker-compose.tools.yml pull +# +# 拉取后可用 docker images 查看所有 edu 相关镜像 +# 这不是常驻服务,只是借用 compose 的批量 pull 能力 + +name: edu-tools + +services: + # ===== CI 运行时镜像(actrunner container job 使用)===== + node-ci: + image: node:22-alpine + command: ["echo", "node-ci image pulled"] + + golang-ci: + image: golang:1.22-alpine + command: ["echo", "golang-ci image pulled"] + + buf-ci: + image: bufbuild/buf:latest + command: ["echo", "buf-ci image pulled"] + + docker-ci: + image: docker:25-git + command: ["echo", "docker-ci image pulled"] + + # ===== 服务构建基础镜像(Dockerfile FROM)===== + # teacher-portal / classes 的 builder 阶段 + node-builder: + image: node:20-alpine + command: ["echo", "node-builder image pulled"] + + # api-gateway 的 builder 阶段 + golang-builder: + image: golang:1.22-alpine + command: ["echo", "golang-builder image pulled"] + + # api-gateway 的 runner 阶段 + alpine-runner: + image: alpine:3.20 + command: ["echo", "alpine-runner image pulled"] + + # ===== 开发/测试基础设施(生产用已有的,不在此管理)===== + # 服务器已有的 MySQL/Redis 不在此拉取 + # 仅用于本地开发测试环境(docker-compose.dev.yml,如有) + mysql-dev: + image: mysql:8.0 + command: ["echo", "mysql-dev image pulled"] + + redis-dev: + image: redis:7-alpine + command: ["echo", "redis-dev image pulled"]