refactor(infra): 重构为 no-push 本地构建部署模式
- 删除 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
This commit is contained in:
60
.github/workflows/ci-go.yml
vendored
60
.github/workflows/ci-go.yml
vendored
@@ -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
|
||||
25
.github/workflows/ci-proto.yml
vendored
25
.github/workflows/ci-proto.yml
vendored
@@ -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
|
||||
33
.github/workflows/ci-py.yml
vendored
33
.github/workflows/ci-py.yml
vendored
@@ -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
|
||||
98
.github/workflows/ci-ts.yml
vendored
98
.github/workflows/ci-ts.yml
vendored
@@ -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 }}
|
||||
197
.github/workflows/ci.yml
vendored
Normal file
197
.github/workflows/ci.yml
vendored
Normal file
@@ -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
|
||||
145
.github/workflows/deploy.yml
vendored
145
.github/workflows/deploy.yml
vendored
@@ -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 <user> -p <token>
|
||||
# 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
|
||||
92
.github/workflows/docker.yml
vendored
92
.github/workflows/docker.yml
vendored
@@ -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/<service>:<tag>
|
||||
# 详见 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<version> + 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
|
||||
@@ -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
|
||||
|
||||
### 15.2 流水线阶段
|
||||
|
||||
| 阶段 | 并行 | 内容 | 失败策略 |
|
||||
| ---------------- | ------------ | ------------------------------------------ | -------- |
|
||||
| **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** | ✅(按服务) | 构建镜像(不推送) | 失败阻断 |
|
||||
| ----------------- | ---- | ------------------------------------ | -------- |
|
||||
| **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 + 健康检查 | 失败阻断 |
|
||||
|
||||
> main 分支额外阶段:`docker-push`(推送镜像)+ `deploy`(部署到服务器)
|
||||
> deploy job 仅在 `push main` 或 `workflow_dispatch` 时触发,PR 时不部署
|
||||
|
||||
### 15.2 触发条件
|
||||
### 15.3 触发条件
|
||||
|
||||
| 事件 | 触发阶段 | 触发条件 |
|
||||
| ------------ | ---------------------------------------------------------- | ---------- |
|
||||
| PR 创建/更新 | lint + typecheck + test + build + arch:scan + docker-build | 所有路径 |
|
||||
| push 到 main | 上述全部 + docker-push + deploy | 合并后自动 |
|
||||
| tag `v*` | docker-push(tag 镜像)+ deploy(生产) | 手动打 tag |
|
||||
| ----------------- | ----------------------------------------------- | -------------------------------- |
|
||||
| PR 创建/更新 | quality-ts + quality-go + quality-proto(并行) | 所有路径 |
|
||||
| push 到 main | 上述全部 + deploy | 合并后自动 |
|
||||
| workflow_dispatch | 上述全部 + deploy | 手动触发,支持 `commit_sha` 回滚 |
|
||||
|
||||
### 15.3 镜像规范
|
||||
> **不再支持 tag 发布**:no-push 模式下不用 `git tag v*` 触发。版本管理通过 commit SHA 追溯。
|
||||
|
||||
- **镜像名**:`edu/<service>:<tag>`
|
||||
- **tag 策略**:
|
||||
- `latest`:main 分支最新
|
||||
- `<git-sha>`:每次构建的 commit SHA(可追溯)
|
||||
- `v<version>`:正式发布 tag
|
||||
- **镜像扫描**:docker-push 后自动运行 Trivy 扫描,CRITICAL 漏洞阻断部署
|
||||
### 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(开发测试)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 核心特点
|
||||
|
||||
- **单文件管理**:一个 `.github/workflows/ci.yml` 管全部 CI/CD
|
||||
- **no-push 本地构建**:不推送到 registry,构建即部署
|
||||
- **不依赖自建镜像**:全部使用官方镜像(node/golang/buf/docker)
|
||||
- **DooD 模式**:deploy job 容器内挂载宿主机 `/var/run/docker.sock`
|
||||
|
||||
### 1.2 组件清单
|
||||
|
||||
| 组件 | 说明 |
|
||||
| ---------------------------- | ---------------------------------------------------------- |
|
||||
| **Gitea** | `git.eazygame.cn`,代码托管 + Actions + Container Registry |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| **Gitea** | `git.eazygame.cn`,代码托管 + Actions |
|
||||
| **Gitea Actions** | CI 运行器,兼容 GitHub Actions 语法 |
|
||||
| **Gitea Container Registry** | 镜像仓库,地址 `git.eazygame.cn/xiner/edu/<service>:<tag>` |
|
||||
| **Deploy Runner** | 跑在服务器上的 actrunner,标签 `ubuntu-latest` |
|
||||
| **服务器 MySQL** | 已有容器,端口 3306,容器名 `edu-mysql`(假设) |
|
||||
| **服务器 Redis** | 已有容器,端口 6379,容器名 `edu-redis`(假设) |
|
||||
| **actrunner** | 跑在服务器上的 runner,标签 `ubuntu-latest` |
|
||||
| **服务器 MySQL** | 已有容器,端口 3306 |
|
||||
| **服务器 Redis** | 已有容器,端口 6379 |
|
||||
|
||||
### 1.2 流水线文件
|
||||
### 1.3 流水线文件
|
||||
|
||||
| 文件 | 触发 | 作用 |
|
||||
| -------------------------------- | ------------------------ | ---------------------------------------------------------- |
|
||||
| `.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 |
|
||||
| --------------------------------- | ---------------------------------- | --------------------------------------------- |
|
||||
| `.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 <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=<openssl rand -hex 32 生成的随机值>
|
||||
# DATABASE_URL=mysql://<user>:<pass>@<mysql容器名>:3306/<db>
|
||||
# REDIS_URL=redis://<redis容器名>:6379
|
||||
|
||||
# 4. 登录 Gitea Container Registry(Runner 用户)
|
||||
docker login git.eazygame.cn -u <user> -p <token>
|
||||
# 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 <user> -p <token>
|
||||
|
||||
### 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/<service>: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>:<tag>
|
||||
```
|
||||
|
||||
| service | 说明 |
|
||||
| ---------------- | --------------- |
|
||||
| `api-gateway` | Go 网关 |
|
||||
| `classes` | NestJS 班级服务 |
|
||||
| `teacher-portal` | Next.js 前端 |
|
||||
|
||||
### 4.2 Tag 策略
|
||||
|
||||
| tag | 来源 | 用途 |
|
||||
| ------------------ | ----------------- | ---------- |
|
||||
| `latest` | main 分支最新构建 | 默认部署 |
|
||||
| `main-<sha>` | main 分支每次构建 | 可追溯 |
|
||||
| `v<version>` | 打 `v*` tag 触发 | 正式版本 |
|
||||
| `v<major>.<minor>` | 打 `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 <bad-commit>
|
||||
@@ -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 <user> -p <token>
|
||||
|
||||
# 或配置 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/<svc>:<tag>` |
|
||||
|
||||
### 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('<file>'))"`
|
||||
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)
|
||||
|
||||
@@ -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 <bad-commit> && 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 |
|
||||
@@ -1,23 +1,27 @@
|
||||
# 服务器部署用 Docker Compose
|
||||
# 镜像来源:Gitea Container Registry (git.eazygame.cn/xiner/edu/<service>:<tag>)
|
||||
# 服务器部署用 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 <user> -p <token>
|
||||
# 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:
|
||||
|
||||
55
infra/docker-compose.tools.yml
Normal file
55
infra/docker-compose.tools.yml
Normal file
@@ -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"]
|
||||
Reference in New Issue
Block a user