feat(ci): GitLab CI/CD pipeline for same-server deployment #370
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| JAVA_VERSION: '17' | |
| NODE_VERSION: '22.x' | |
| PNPM_VERSION: '10' | |
| jobs: | |
| # --- Changes detection --- | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| backend: ${{ steps.filter.outputs.backend }} | |
| console: ${{ steps.filter.outputs.console }} | |
| management: ${{ steps.filter.outputs.management }} | |
| docker: ${{ steps.filter.outputs.docker }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Detect changed paths | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| backend: | |
| - 'backend-spring/**' | |
| - 'init-db/migrations/**' | |
| - '.github/workflows/ci.yml' | |
| console: | |
| - 'console/**' | |
| - 'shared/**' | |
| - '.github/workflows/ci.yml' | |
| management: | |
| - 'management/**' | |
| - 'shared/**' | |
| - '.github/workflows/ci.yml' | |
| docker: | |
| - 'backend-spring/Dockerfile' | |
| - 'console/Dockerfile' | |
| - 'management/Dockerfile' | |
| - 'docker-compose*.yml' | |
| - '.dockerignore' | |
| - 'console/nginx.conf' | |
| - 'management/nginx.conf' | |
| - '.github/workflows/ci.yml' | |
| # --- Backend: Build --- | |
| backend-build: | |
| name: Backend Build | |
| needs: changes | |
| if: needs.changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| cache: 'maven' | |
| - name: Grant Maven wrapper execute permission | |
| run: chmod +x backend-spring/mvnw | |
| - name: Build with Maven | |
| run: cd backend-spring && ./mvnw compile -B | |
| # --- Backend: Test --- | |
| backend-test: | |
| name: Backend Test | |
| needs: [changes, backend-build] | |
| if: needs.changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| services: | |
| mysql: | |
| image: mysql:9.1 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: ulticode | |
| MYSQL_USER: ulticode | |
| MYSQL_PASSWORD: ulticode | |
| ports: | |
| - 23306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h localhost -u root -proot" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 26379:6379 | |
| options: >- | |
| --health-cmd="redis-cli ping" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| env: | |
| DB_HOST: localhost | |
| DB_PORT: 23306 | |
| DB_USER: ulticode | |
| DB_PASSWORD: ulticode | |
| DB_NAME: ulticode | |
| REDIS_HOST: localhost | |
| REDIS_PORT: 26379 | |
| JWT_SECRET: test-jwt-secret-key-for-ci-minimum-32-characters-long | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| cache: 'maven' | |
| - name: Grant Maven wrapper execute permission | |
| run: chmod +x backend-spring/mvnw | |
| - name: Run tests with CI profile | |
| run: cd backend-spring && ./mvnw test -Dspring.profiles.active=ci -Dtest='!*IT' -B | |
| - name: Upload test results on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-backend | |
| path: backend-spring/target/surefire-reports/ | |
| retention-days: 7 | |
| # --- Backend: Test under feature flag matrix (ADR-005 §4 #4) --- | |
| # 与 backend-test 同结构, 改 matrix 化: features=[off,on] 两个变体覆盖 | |
| # "全关" 与 "产品全开" 路径, 防 "默认 flag 路径 vs 全开/全关路径" 行为漂移. | |
| # cutover flag 在所有变体都保持 false (走 cutover 守门, 不在 CI 强开). | |
| # 第三个 default 路径由原 backend-test job 覆盖 (避免动 PR status check). | |
| # R-R1 修复: 原 backend-test-features-{off,on} 2 job 复制 162 行 services+steps, | |
| # 现 matrix 化只 1 job 90 行, 改 backend-test 模板不再漏改. | |
| backend-test-features: | |
| name: Backend Test (features ${{ matrix.features }}) | |
| needs: [changes, backend-build] | |
| if: needs.changes.outputs.backend == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| features: [off, on] | |
| runs-on: ubuntu-latest | |
| services: | |
| mysql: | |
| image: mysql:9.1 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: ulticode | |
| MYSQL_USER: ulticode | |
| MYSQL_PASSWORD: ulticode | |
| ports: | |
| - 23306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h localhost -u root -proot" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 26379:6379 | |
| options: >- | |
| --health-cmd="redis-cli ping" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| env: | |
| DB_HOST: localhost | |
| DB_PORT: 23306 | |
| DB_USER: ulticode | |
| DB_PASSWORD: ulticode | |
| DB_NAME: ulticode | |
| REDIS_HOST: localhost | |
| REDIS_PORT: 26379 | |
| JWT_SECRET: test-jwt-secret-key-for-ci-minimum-32-characters-long | |
| # 5 cutover flag 全 false (matrix 无关, 走 cutover 守门) | |
| USE_JUDGE_OUTBOX: 'false' | |
| USE_GENERATION_FENCE: 'false' | |
| JUDGE_QUEUE_USE_PORT: 'false' | |
| JUDGE_QUEUE_ENVELOPE_VERSION: '1' | |
| JUDGE_QUEUE_CUTOVER_AT: '1970-01-01T00:00:00' | |
| USE_NOTIFICATION_INTENT: 'false' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| cache: 'maven' | |
| - name: Grant Maven wrapper execute permission | |
| run: chmod +x backend-spring/mvnw | |
| # 5 个产品 flag 由 matrix.features 决定 on/off, 通过 env 注入到 step. | |
| # Spring profile 同时追加 features-${{ matrix.features }}, 与 yml profile | |
| # 形成双保险 (yml 提供默认值, env 覆盖兜底). | |
| - name: Inject feature flag env vars from matrix | |
| run: | | |
| if [[ "${{ matrix.features }}" == "on" ]]; then | |
| echo "USE_NEW_CONTEST_SYSTEM=true" >> "$GITHUB_ENV" | |
| echo "REALTIME_RANKING_ENABLED=true" >> "$GITHUB_ENV" | |
| echo "FIRST_SOLVE_NOTIFICATIONS_ENABLED=true" >> "$GITHUB_ENV" | |
| echo "ANTICHEAT_ENABLED=true" >> "$GITHUB_ENV" | |
| echo "CONTEST_ANALYTICS_ENABLED=true" >> "$GITHUB_ENV" | |
| else | |
| echo "USE_NEW_CONTEST_SYSTEM=false" >> "$GITHUB_ENV" | |
| echo "REALTIME_RANKING_ENABLED=false" >> "$GITHUB_ENV" | |
| echo "FIRST_SOLVE_NOTIFICATIONS_ENABLED=false" >> "$GITHUB_ENV" | |
| echo "ANTICHEAT_ENABLED=false" >> "$GITHUB_ENV" | |
| echo "CONTEST_ANALYTICS_ENABLED=false" >> "$GITHUB_ENV" | |
| fi | |
| - name: Run tests with features-${{ matrix.features }} profile | |
| run: cd backend-spring && ./mvnw test -Dspring.profiles.active=ci,features-${{ matrix.features }} -Dtest='!*IT' -B | |
| - name: Upload test results on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-backend-features-${{ matrix.features }} | |
| path: backend-spring/target/surefire-reports/ | |
| retention-days: 7 | |
| # --- Backend: Validate Migrations --- | |
| migrate-validate: | |
| name: Validate Migrations | |
| needs: changes | |
| if: needs.changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| services: | |
| mysql: | |
| image: mysql:9.1 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: ulticode_test | |
| MYSQL_USER: ulticode | |
| MYSQL_PASSWORD: ulticode | |
| ports: | |
| - 23306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h localhost -u root -proot" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| env: | |
| DB_HOST: localhost | |
| DB_PORT: 23306 | |
| DB_USER: ulticode | |
| DB_PASSWORD: ulticode | |
| DB_NAME: ulticode_test | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Wait for MySQL | |
| run: | | |
| for i in $(seq 1 30); do | |
| if mysqladmin ping -h localhost -P 23306 -u ulticode -pulticode 2>/dev/null; then | |
| echo "MySQL is ready" | |
| break | |
| fi | |
| echo "Waiting for MySQL... ($i/30)" | |
| sleep 2 | |
| done | |
| - name: Run and validate migrations | |
| run: | | |
| docker run --rm --network host \ | |
| -v "$PWD/init-db/migrations:/flyway/sql:ro" \ | |
| flyway/flyway:10.17.0 \ | |
| -url="jdbc:mysql://127.0.0.1:23306/ulticode?allowPublicKeyRetrieval=true&useSSL=false" \ | |
| -user=ulticode -password=ulticode -connectRetries=10 migrate | |
| docker run --rm --network host \ | |
| -v "$PWD/init-db/migrations:/flyway/sql:ro" \ | |
| flyway/flyway:10.17.0 \ | |
| -url="jdbc:mysql://127.0.0.1:23306/ulticode?allowPublicKeyRetrieval=true&useSSL=false" \ | |
| -user=ulticode -password=ulticode validate | |
| secret-scan: | |
| name: Secret Scan | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Run Gitleaks | |
| run: | | |
| docker run --rm \ | |
| -v "$PWD:/repo:ro" \ | |
| zricethezav/gitleaks:v8.24.2 \ | |
| dir /repo --config=/repo/.gitleaks.toml --redact --no-banner | |
| # --- Backend: Flyway Filename Lint --- | |
| # ADR-005 §4 #5: DB 迁移文件名遵守 V{ts}__{description}.sql (CLAUDE.md 约定) | |
| # 正则 ^V[0-9]{8}_?[0-9_]*__[A-Za-z0-9_]+\.sql$ 兼容两种时间戳写法 | |
| # (V20260604_110000 与 V20260604110000), 与现有 28 个文件全部合规. | |
| flyway-filename-lint: | |
| name: Flyway Filename Lint | |
| needs: changes | |
| if: needs.changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Lint migration filenames | |
| run: | | |
| set -e | |
| PATTERN='^V[0-9]{8}_?[0-9_]*__[A-Za-z0-9_]+\.sql$' | |
| cd init-db/migrations | |
| fails=0 | |
| total=0 | |
| for f in V*.sql; do | |
| [[ -e "$f" ]] || continue | |
| total=$((total+1)) | |
| if [[ ! "$f" =~ $PATTERN ]]; then | |
| echo "::error file=$f::must match $PATTERN (V<14-digit-ts>__<desc>.sql)" | |
| fails=$((fails+1)) | |
| fi | |
| done | |
| if [[ $fails -gt 0 ]]; then | |
| echo "::error::$fails of $total migration file(s) violate naming convention" | |
| exit 1 | |
| fi | |
| echo "✅ all $total migration filenames conform to V<14-digit-ts>__<desc>.sql" | |
| # --- Frontend: Lint --- | |
| frontend-lint: | |
| name: Lint (${{ matrix.app }}) | |
| needs: changes | |
| if: needs.changes.outputs.console == 'true' || needs.changes.outputs.management == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: [console, management] | |
| steps: | |
| - name: Check if app changed | |
| id: should-run | |
| if: (matrix.app == 'console' && needs.changes.outputs.console == 'true') || (matrix.app == 'management' && needs.changes.outputs.management == 'true') | |
| run: echo "run=true" >> $GITHUB_OUTPUT | |
| - name: Checkout repository | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Enable pnpm via corepack | |
| if: steps.should-run.outputs.run == 'true' | |
| run: corepack enable && corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate | |
| - name: Setup Node.js | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| cache-dependency-path: ${{ matrix.app }}/pnpm-lock.yaml | |
| - name: Install dependencies | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| env: | |
| CI: "false" | |
| GITHUB_ACTIONS: "false" | |
| run: pnpm install | |
| - name: Run lint | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| run: pnpm lint | |
| # --- Frontend: Type Check --- | |
| frontend-type-check: | |
| name: Type Check (${{ matrix.app }}) | |
| needs: changes | |
| if: needs.changes.outputs.console == 'true' || needs.changes.outputs.management == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: [console, management] | |
| steps: | |
| - name: Check if app changed | |
| id: should-run | |
| if: (matrix.app == 'console' && needs.changes.outputs.console == 'true') || (matrix.app == 'management' && needs.changes.outputs.management == 'true') | |
| run: echo "run=true" >> $GITHUB_OUTPUT | |
| - name: Checkout repository | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Enable pnpm via corepack | |
| if: steps.should-run.outputs.run == 'true' | |
| run: corepack enable && corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate | |
| - name: Setup Node.js | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| cache-dependency-path: ${{ matrix.app }}/pnpm-lock.yaml | |
| - name: Install dependencies | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| env: | |
| CI: "false" | |
| GITHUB_ACTIONS: "false" | |
| run: pnpm install | |
| - name: Run type-check | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| run: pnpm type-check | |
| # --- Frontend: Test --- | |
| frontend-test: | |
| name: Test (${{ matrix.app }}) | |
| needs: changes | |
| if: needs.changes.outputs.console == 'true' || needs.changes.outputs.management == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: [console, management] | |
| steps: | |
| - name: Check if app changed | |
| id: should-run | |
| if: (matrix.app == 'console' && needs.changes.outputs.console == 'true') || (matrix.app == 'management' && needs.changes.outputs.management == 'true') | |
| run: echo "run=true" >> $GITHUB_OUTPUT | |
| - name: Checkout repository | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Enable pnpm via corepack | |
| if: steps.should-run.outputs.run == 'true' | |
| run: corepack enable && corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate | |
| - name: Setup Node.js | |
| if: steps.should-run.outputs.run == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| cache-dependency-path: ${{ matrix.app }}/pnpm-lock.yaml | |
| - name: Install dependencies | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| env: | |
| CI: "false" | |
| GITHUB_ACTIONS: "false" | |
| run: pnpm install | |
| - name: Run tests | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| run: pnpm test | |
| - name: Audit production dependencies | |
| if: steps.should-run.outputs.run == 'true' | |
| working-directory: ${{ matrix.app }} | |
| run: pnpm audit --prod --audit-level high | |
| shared-auth-test: | |
| name: Test shared auth core | |
| needs: changes | |
| if: needs.changes.outputs.console == 'true' || needs.changes.outputs.management == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Enable pnpm via corepack | |
| run: corepack enable && corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| cache-dependency-path: shared/auth-core/pnpm-lock.yaml | |
| - name: Install dependencies | |
| working-directory: shared/auth-core | |
| run: pnpm install --frozen-lockfile | |
| - name: Test and type-check | |
| working-directory: shared/auth-core | |
| run: pnpm test && pnpm type-check | |
| # --- Frontend: i18n Check (management only) --- | |
| frontend-i18n-check: | |
| name: i18n Check (management) | |
| needs: changes | |
| if: needs.changes.outputs.management == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Enable pnpm via corepack | |
| run: corepack enable && corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| cache-dependency-path: management/pnpm-lock.yaml | |
| - name: Install dependencies | |
| working-directory: management | |
| env: | |
| CI: "false" | |
| GITHUB_ACTIONS: "false" | |
| run: pnpm install | |
| - name: Run i18n completeness check | |
| working-directory: management | |
| run: pnpm check:i18n --json | |
| - name: Run i18n tests | |
| working-directory: management | |
| run: pnpm vitest run -- src/i18n/__tests__/ | |
| # --- Docker Build Verification --- | |
| docker-verify: | |
| name: Docker Build (${{ matrix.service.name }}) | |
| needs: changes | |
| if: needs.changes.outputs.docker == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| service: | |
| - name: backend | |
| dockerfile: ./backend-spring/Dockerfile | |
| - name: console | |
| dockerfile: ./console/Dockerfile | |
| - name: management | |
| dockerfile: ./management/Dockerfile | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build Docker image (no push) | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ${{ matrix.service.dockerfile }} | |
| push: false | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |