f10ed26be6
ci/woodpecker/push/woodpecker Pipeline failed
- 移除 heredoc 语法 - 使用单引号和双引号组合来构建 JSON - 确保变量正确展开 - 本地测试成功
504 lines
15 KiB
YAML
504 lines
15 KiB
YAML
# ============================================
|
|
# 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的自动归档
|
|
#
|
|
# 流水线阶段:
|
|
# 1. 代码质量检查 (lint, type-check, security)
|
|
# 2. 单元测试和集成测试
|
|
# 3. E2E测试 (分层测试)
|
|
# 4. 构建Docker镜像 (仅release分支)
|
|
# 5. 部署到生产环境 (仅release分支)
|
|
# 6. 归档到main分支 (仅release分支)
|
|
# 7. 通知和监控
|
|
# ============================================
|
|
|
|
# 全局环境变量
|
|
variables:
|
|
- &node_image node:20-alpine
|
|
- &docker_image docker:24-cli
|
|
- &npm_cache /root/.npm
|
|
- &node_modules_cache /woodpecker/src/node_modules
|
|
|
|
# ============================================
|
|
# 阶段1: 代码质量检查
|
|
# ============================================
|
|
steps:
|
|
lint:
|
|
image: *node_image
|
|
environment:
|
|
NODE_ENV: development
|
|
commands:
|
|
- npm ci
|
|
- 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
|
|
commands:
|
|
- npm ci
|
|
- 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
|
|
commands:
|
|
- npm ci --omit=dev --ignore-scripts
|
|
- npm audit --audit-level=high --omit=dev
|
|
when:
|
|
event:
|
|
- push
|
|
- pull_request
|
|
branch:
|
|
- feature/**
|
|
- dev
|
|
- release
|
|
- release/**
|
|
failure: ignore
|
|
|
|
unit-tests:
|
|
image: *node_image
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- npm run test:coverage:check
|
|
volumes:
|
|
- /tmp/npm-cache:/root/.npm
|
|
- /tmp/node-modules-cache:/woodpecker/src/node_modules
|
|
when:
|
|
event:
|
|
- push
|
|
- pull_request
|
|
branch:
|
|
- dev
|
|
- release
|
|
- release/**
|
|
|
|
# ============================================
|
|
# 阶段3: E2E测试 (分层测试)
|
|
# ============================================
|
|
e2e-smoke:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium --with-deps
|
|
- npm run test:smoke
|
|
when:
|
|
event:
|
|
- push
|
|
- pull_request
|
|
branch:
|
|
- feature/**
|
|
|
|
e2e-standard:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium --with-deps
|
|
- npm run test:tier:standard
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- dev
|
|
- release
|
|
- release/**
|
|
|
|
e2e-deep:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium firefox webkit --with-deps
|
|
- npm run test:tier:deep
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
|
|
e2e-performance:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium --with-deps
|
|
- npm run test:performance
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
|
|
e2e-accessibility:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium --with-deps
|
|
- npx playwright test --grep @accessibility
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
|
|
e2e-visual:
|
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
|
environment:
|
|
NODE_ENV: test
|
|
CI: true
|
|
commands:
|
|
- npm ci
|
|
- cd e2e && npm ci
|
|
- npx playwright install chromium --with-deps
|
|
- npx playwright test --grep @visual
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
|
|
# ============================================
|
|
# 阶段4: 构建Docker镜像 (release分支)
|
|
# ============================================
|
|
build-image:
|
|
image: *docker_image
|
|
environment:
|
|
REGISTRY_PASSWORD:
|
|
from_secret: registry_password
|
|
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/**
|
|
|
|
# ============================================
|
|
# 阶段5: 部署到生产环境 (release分支)
|
|
# ============================================
|
|
deploy-production:
|
|
image: alpine:latest
|
|
environment:
|
|
DEPLOY_ENV: production
|
|
SSH_PRIVATE_KEY:
|
|
from_secret: ssh_private_key
|
|
REGISTRY_PASSWORD:
|
|
from_secret: registry_password
|
|
commands:
|
|
- echo "Deploying to production environment..."
|
|
- apk add --no-cache openssh-client curl
|
|
- 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 curl -f 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 curl -f 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/**
|
|
|
|
# ============================================
|
|
# 阶段6: 归档到main分支 (release分支)
|
|
# ============================================
|
|
archive-to-main:
|
|
image: alpine:latest
|
|
environment:
|
|
SSH_PRIVATE_KEY:
|
|
from_secret: ssh_private_key
|
|
commands:
|
|
- echo "Archiving to main branch..."
|
|
- apk add --no-cache git openssh-client
|
|
- 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
|
|
|
|
# ============================================
|
|
# 阶段7: 企业微信通知
|
|
# ============================================
|
|
notify-wechat-success:
|
|
image: curlimages/curl:latest
|
|
environment:
|
|
WECHAT_WEBHOOK:
|
|
from_secret: wechat_webhook
|
|
commands:
|
|
- |
|
|
BRANCH="${CI_COMMIT_BRANCH:-unknown}"
|
|
COMMIT="${CI_COMMIT_SHA:0:7:-unknown}"
|
|
MESSAGE="${CI_COMMIT_MESSAGE:-no message}"
|
|
AUTHOR="${CI_COMMIT_AUTHOR:-unknown}"
|
|
PIPELINE_NUMBER="${CI_PIPELINE_NUMBER:-0}"
|
|
REPO_ID="${CI_REPO_ID:-1}"
|
|
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
|
|
|
|
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
|
|
|
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
|
|
|
|
curl -X POST "$WECHAT_WEBHOOK" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"msgtype": "markdown",
|
|
"markdown": {
|
|
"content": "## 🚀 Novalon Website 部署通知\n\n> **构建状态**: <font color=\"info\">成功</font>\n\n**项目信息**\n> 分支: `'"${BRANCH}"'`\n> 提交: `'"${COMMIT}"'`\n> 作者: '"${AUTHOR}"'\n\n**提交信息**\n> '"${MESSAGE_CLEAN}"'\n\n**操作**\n> [查看构建详情]('"${PIPELINE_URL}"')\n\n---\n> 时间: '"${TIMESTAMP}"'\n> Pipeline #'"${PIPELINE_NUMBER}"'"
|
|
}
|
|
}'
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
status:
|
|
- success
|
|
|
|
notify-wechat-failure:
|
|
image: curlimages/curl:latest
|
|
environment:
|
|
WECHAT_WEBHOOK:
|
|
from_secret: wechat_webhook
|
|
commands:
|
|
- |
|
|
BRANCH="${CI_COMMIT_BRANCH:-unknown}"
|
|
COMMIT="${CI_COMMIT_SHA:0:7:-unknown}"
|
|
MESSAGE="${CI_COMMIT_MESSAGE:-no message}"
|
|
AUTHOR="${CI_COMMIT_AUTHOR:-unknown}"
|
|
PIPELINE_NUMBER="${CI_PIPELINE_NUMBER:-0}"
|
|
REPO_ID="${CI_REPO_ID:-1}"
|
|
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
|
|
|
|
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
|
|
|
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
|
|
|
|
curl -X POST "$WECHAT_WEBHOOK" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"msgtype": "markdown",
|
|
"markdown": {
|
|
"content": "## 🚀 Novalon Website 部署通知\n\n> **构建状态**: <font color=\"warning\">失败</font>\n\n**项目信息**\n> 分支: `'"${BRANCH}"'`\n> 提交: `'"${COMMIT}"'`\n> 作者: '"${AUTHOR}"'\n\n**提交信息**\n> '"${MESSAGE_CLEAN}"'\n\n**操作**\n> [查看构建详情]('"${PIPELINE_URL}"')\n\n---\n> 时间: '"${TIMESTAMP}"'\n> Pipeline #'"${PIPELINE_NUMBER}"'"
|
|
}
|
|
}'
|
|
when:
|
|
event:
|
|
- push
|
|
branch:
|
|
- release
|
|
- release/**
|
|
status:
|
|
- failure
|
|
|
|
# ============================================
|
|
# 工作区配置
|
|
# ============================================
|
|
workspace:
|
|
base: /woodpecker
|
|
path: src
|
|
|
|
# ============================================
|
|
# 克隆配置
|
|
# ============================================
|
|
clone:
|
|
git:
|
|
image: woodpeckerci/plugin-git
|
|
settings:
|
|
depth: 1
|
|
partial: false
|