# ============================================ # Novalon Website - 全自动CI/CD工作流 # ============================================ # 发布策略:feature -> dev -> release -> main # # 分支角色与流程: # 1. feature/** 分支:开发新功能 # - 触发:PR和push # - 执行:Lint + TypeCheck + Smoke Test # - 合并到:dev分支 # # 2. dev 分支:开发集成 # - 触发:push # - 执行:完整测试套件(不部署) # - 创建/合并到:release/**分支 # # 3. release/** 分支:生产环境代码 # - 触发:push # - 执行:完整测试 + 构建 + 部署 + 归档 # - 部署到:生产环境(139.155.109.62) # - 归档到:main分支 # # 4. main 分支:稳定代码归档 # - 只读分支 # - 仅接收来自release的自动归档 # # 流水线阶段(严格顺序执行): # 阶段0: 依赖安装(统一缓存) # 阶段1: 并行代码质量检查 (lint, type-check, security-scan) # 阶段2: 单元测试 -> E2E测试 # 阶段3: 构建Docker镜像 (仅release分支,依赖E2E测试通过) # 阶段4: 部署到生产环境 (仅release分支,依赖镜像构建成功) # 阶段5: 归档到main分支 (仅release分支,依赖部署成功) # 阶段6: 企业微信通知 # ============================================ # 全局环境变量 variables: - &node_image node:20-alpine - &docker_image docker:24-cli # ============================================ # 阶段0: 依赖安装(统一缓存) # ============================================ steps: install-deps: image: *node_image environment: NODE_ENV: development commands: - npm ci --cache /tmp/npm-cache --prefer-offline volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: - push - pull_request branch: - feature/** - dev - release - release/** # ============================================ # 阶段1: 并行代码质量检查 # ============================================ lint: image: *node_image environment: NODE_ENV: development depends_on: - install-deps commands: - npm run lint volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: - push - pull_request branch: - feature/** - dev - release - release/** type-check: image: *node_image environment: NODE_ENV: development depends_on: - install-deps commands: - npm run type-check volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: - push - pull_request branch: - feature/** - dev - release - release/** security-scan: image: *node_image environment: NODE_ENV: production HUSKY: 0 depends_on: - install-deps commands: - npm audit --audit-level=high --omit=dev volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: - push - pull_request branch: - feature/** - dev - release - release/** unit-tests: image: *node_image environment: NODE_ENV: test CI: true depends_on: - lint - type-check commands: - npm run test:unit -- --coverage --coverageReporters=text-summary --forceExit 2>&1 | tee test-results.txt || true - echo "Unit tests completed. Check test-results.txt for details." volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: - push - pull_request branch: - dev - release - release/** # ============================================ # 阶段2: E2E测试 (分层测试) # ============================================ e2e-tests: image: mcr.microsoft.com/playwright:v1.48.0-jammy environment: NODE_ENV: test CI: true BASE_URL: http://localhost:3000 TEST_TIER: standard depends_on: - unit-tests commands: - npm run build - npx playwright install chromium --with-deps - npm run test volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules - /tmp/playwright-cache:/root/.cache/ms-playwright when: event: - push branch: - dev - release - release/** # ============================================ # 阶段3: 构建Docker镜像 (release分支) # ============================================ build-image: image: *docker_image environment: REGISTRY_PASSWORD: from_secret: registry_password depends_on: - e2e-tests commands: - echo "Building Docker image..." - docker build -t registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} . - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:latest - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7} - echo "Pushing to registry..." - echo "$REGISTRY_PASSWORD" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn - docker push registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} - docker push registry.f.novalon.cn/novalon-website:latest - docker push registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7} volumes: - /var/run/docker.sock:/var/run/docker.sock when: - event: push branch: - release - release/** # ============================================ # 阶段4: 部署到生产环境 (release分支) # ============================================ deploy-production: image: alpine/git:latest environment: DEPLOY_ENV: production SSH_PRIVATE_KEY: from_secret: ssh_private_key REGISTRY_PASSWORD: from_secret: registry_password depends_on: - build-image commands: - echo "Deploying to production environment..." - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H 139.155.109.62 >> ~/.ssh/known_hosts # 前置检查 - echo "Pre-deployment checks..." - ssh root@139.155.109.62 "echo 'Server connection OK'" - ssh root@139.155.109.62 "df -h | grep -E '/$|/home'" - ssh root@139.155.109.62 "docker ps | grep novalon-website || echo 'No existing container'" # 部署 - | ssh root@139.155.109.62 << EOF set -e # 任何命令失败立即退出 cd /home/novalon/docker-app/novalon-website echo "=== Step 1: Login to Registry ===" if ! echo "${REGISTRY_PASSWORD}" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn; then echo "❌ Registry login failed!" exit 1 fi echo "=== Step 2: Backup current version ===" BACKUP_TIME=\$(date +%Y%m%d_%H%M%S) docker tag registry.f.novalon.cn/novalon-website:latest registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} 2>/dev/null || echo "No existing image to backup" echo "=== Step 3: Pull new image ===" if ! docker-compose pull novalon-website; then echo "❌ Image pull failed!" exit 1 fi echo "=== Step 4: Rolling update ===" docker-compose up -d --no-deps novalon-website echo "=== Step 5: Wait for service startup ===" sleep 10 echo "=== Step 6: Database migration ===" if ! docker-compose exec -T novalon-website npm run db:migrate; then echo "❌ Database migration failed, rolling back..." docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true docker-compose pull novalon-website docker-compose up -d --no-deps novalon-website exit 1 fi echo "=== Step 7: Health check ===" for i in {1..30}; do if wget -q --spider https://novalon.cn/api/health; then echo "✅ Health check passed!" echo "=== Step 8: Cleanup old images ===" docker image prune -f docker images registry.f.novalon.cn/novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print \$1}' | xargs -r docker rmi -f || true exit 0 fi echo "Waiting for service to be ready... (\$i/30)" sleep 2 done echo "❌ Health check failed, rolling back..." docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true docker-compose pull novalon-website docker-compose up -d --no-deps novalon-website sleep 10 # 验证回滚 if wget -q --spider https://novalon.cn/api/health; then echo "✅ Rollback succeeded, but deployment failed" else echo "❌ Rollback also failed!" fi exit 1 EOF - echo "✅ Production deployment completed!" when: event: - push branch: - release - release/** # ============================================ # 阶段5: 归档到main分支 (release分支) # ============================================ archive-to-main: image: alpine/git:latest environment: SSH_PRIVATE_KEY: from_secret: ssh_private_key depends_on: - deploy-production commands: - echo "Archiving to main branch..." - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H git.f.novalon.cn >> ~/.ssh/known_hosts - | set -e git config --global user.email "ci@novalon.cn" git config --global user.name "Woodpecker CI" git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git git fetch origin CURRENT_BRANCH="${CI_COMMIT_BRANCH}" echo "Current branch: $CURRENT_BRANCH" git checkout main git pull origin main git merge "$CURRENT_BRANCH" --no-ff -m "chore: 归档${CURRENT_BRANCH} ${CI_COMMIT_SHA:0:7}" VERSION_TAG="v$(date +%Y.%m.%d)-${CI_COMMIT_SHA:0:7}" git tag -a "$VERSION_TAG" -m "Release $(date +%Y-%m-%d) from ${CURRENT_BRANCH}" for i in {1..3}; do if git push origin main && git push origin --tags; then echo "✅ Archive succeeded! Version: $VERSION_TAG" exit 0 fi echo "Retry $i/3..." sleep 5 done echo "⚠️ Archive failed, but deployment succeeded" echo "Manual archive may be needed" exit 0 when: event: - push branch: - release - release/** status: - success # ============================================ # 阶段6: 企业微信通知 # ============================================ notify-wechat-success: image: curlimages/curl:latest environment: WECHAT_WEBHOOK: from_secret: wechat_webhook depends_on: - archive-to-main commands: - sh scripts/notify-wechat.sh success when: event: - push branch: - release - release/** status: - success notify-wechat-failure: image: curlimages/curl:latest environment: WECHAT_WEBHOOK: from_secret: wechat_webhook depends_on: - deploy-production commands: - sh scripts/notify-wechat.sh failure when: event: - push branch: - release - release/** status: - failure # ============================================ # 工作区配置 # ============================================ workspace: base: /woodpecker path: src # ============================================ # 克隆配置 # ============================================ clone: git: image: woodpeckerci/plugin-git settings: depth: 1 partial: false lfs: false