fix(cicd): 修复 web 流水线 - 路径/构建脚本/飞书凭证/部署方式

- Dockerfile: apps/web-antd → apps/web-antdv-next(实际落点)
- Dockerfile: pnpm build:antd(不存在)→ turbo build --filter=@vben/web-antdv-next
- Dockerfile: 新增 nginx.conf(原 COPY 目标文件不存在导致 build 失败)
- Jenkinsfile: 移除飞书通知(凭证 feishu-* 未配,导致构建启动即 FAIL)
- Jenkinsfile: 部署 node(deploy-agent)→ SSH 直连 cd-target(无该 agent 节点)
- Jenkinsfile: 清除硬编码 webhook URL 明文
This commit is contained in:
xiangning 2026-07-17 11:21:46 +08:00
parent 29f3b3b451
commit 5696168bc3
3 changed files with 83 additions and 65 deletions

View File

@ -1,30 +1,32 @@
# BuildFlow Web (Vben Admin - web-antd) 多阶段构建
# BuildFlow Web (web-antdv-next / @vben/web-antdv-next) 多阶段构建
# 第一阶段:Node 22 构建 SPA 产物;第二阶段:Nginx 托管静态资源
FROM node:22-alpine AS builder
WORKDIR /app
# 全局装 pnpm (与项目对齐)
RUN npm install -g pnpm@9
# 全局装 pnpm(与本地 11.x 对齐;corepack 在 alpine 上不可靠)
RUN npm install -g pnpm@11
# 先拷依赖清单利用层缓存
# 先拷依赖清单利用层缓存(pnpm workspace + turbo 需要这些清单文件齐备)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY apps/web-antd/package.json ./apps/web-antd/package.json
# 拷贝 internal/scripts (turbo/vsh 工具链需要)
COPY internal/ ./internal/
COPY scripts/ ./scripts/
COPY apps/web-antdv-next/package.json ./apps/web-antdv-next/package.json
# 拷贝其余 packages 的 package.json(workspace 依赖解析需要),但不拷 node_modules
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
# 拷贝全部源码
COPY . .
# 构建 web-antd 变体 (质量门禁: lint + typecheck 内含于 build)
RUN pnpm build:antd
# 构建 web-antdv-next 变体(turbo 按 workspace 名过滤;质量门禁内含 typecheck)
RUN pnpm run build --filter=@vben/web-antdv-next...
# ---- 运行阶段: nginx 托管静态资源 ----
# ---- 运行阶段:Nginx 托管静态资源 ----
FROM nginx:1.27-alpine AS runtime
COPY apps/web-antd/nginx.conf /etc/nginx/conf.d/default.conf 2>/dev/null || true
COPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html
COPY apps/web-antdv-next/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/apps/web-antdv-next/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -q --spider http://127.0.0.1/ || exit 1
CMD ["nginx", "-g", "daemon off;"]

84
Jenkinsfile vendored
View File

@ -1,42 +1,6 @@
// BuildFlow buildflow-web CI/CD - clean version
// ①拉取代码 -> ②编译打包 -> 推送Harbor -> ③部署 + 飞书通知
def feishuNotify = { String result, String failedStageName = "" ->
def dur = currentBuild.durationString.replace(" and counting", "")
def commit = ""
try { commit = sh(script: "git log -1 --pretty=format:%s", returnStdout: true).trim() } catch(ignored) {}
def isOk = (result == "SUCCESS")
def color = isOk ? "green" : "red"
def statusText = isOk ? "成功" : "失败"
def title = "CI/CD " + statusText + " " + env.JOB_NAME + " #" + env.BUILD_NUMBER
def fields = [
[is_short: true, text: [tag: "lark_md", content: "**项目**\n" + env.JOB_NAME]],
[is_short: true, text: [tag: "lark_md", content: "**耗时**\n" + dur]],
[is_short: true, text: [tag: "lark_md", content: "**提交**\n" + commit.take(50)]],
[is_short: true, text: [tag: "lark_md", content: "**状态**\n" + statusText]]
]
def elements = [[tag: "div", fields: fields], [tag: "hr"]]
if (isOk) {
elements.add([tag: "div", text: [tag: "lark_md", content: "镜像地址\n`" + env.IMAGE + ":" + env.TAG + "`"]])
} else if (failedStageName) {
elements.add([tag: "div", text: [tag: "lark_md", content: "失败阶段\n" + failedStageName]])
}
def burl = env.BUILD_URL
if (burl == null) { burl = "http://115.191.51.246:8080/job/" + env.JOB_NAME.replace("/", "/job/") + "/" }
elements.add([tag: "action", actions: [[tag: "button", text: [tag: "plain_text", content: "查看构建"], url: burl, type: isOk ? "primary" : "danger"]]])
def card = [msg_type: "interactive", card: [header: [title: [tag: "plain_text", content: title], template: color], elements: elements]]
def ts = String.valueOf(System.currentTimeMillis() / 1000L as long)
def stringToSign = ts + "\n" + "Qp6SwLIkgRJEtV9UsOf6mg"
def mac = javax.crypto.Mac.getInstance("HmacSHA256")
mac.init(new javax.crypto.spec.SecretKeySpec(stringToSign.getBytes("UTF-8"), "HmacSHA256"))
def sign = mac.doFinal().encodeBase64().toString()
card.timestamp = ts
card.sign = sign
def jsonStr = groovy.json.JsonOutput.toJson(card)
writeFile file: "/tmp/feishu_msg.json", text: jsonStr
sh 'curl -sS -X POST https://open.feishu.cn/open-apis/bot/v2/hook/921d9874-de0a-4eca-9ca6-66bf91bd2fbc -H "Content-Type: application/json" -d @/tmp/feishu_msg.json || true'
sh 'rm -f /tmp/feishu_msg.json'
}
// BuildFlow buildflow-web CI/CD
// 链路:①拉取代码 → ②编译打包(docker build) → 推送 Harbor → ③部署(cd-target SSH) → HealthCheck
// 注:飞书通知逻辑已移除(凭证未配齐);部署通过 ssh 直连 cd-target 执行。
pipeline {
agent any
@ -44,6 +8,9 @@ pipeline {
HARBOR = "43.134.235.58:58080"
IMAGE = "${HARBOR}/buildflow/buildflow-web"
TAG = "${env.BUILD_NUMBER}"
DEPLOY_HOST = "cd-target"
APP_PORT = "80" // 容器内 nginx 监听 80;宿主映射 5175:80
HOST_PORT = "5175"
}
options {
timeout(time: 30, unit: "MINUTES")
@ -78,29 +45,40 @@ pipeline {
steps {
script {
try {
node("deploy-agent") {
sh '''
docker pull $IMAGE:latest
sh """
ssh -o StrictHostKeyChecking=accept-new ${DEPLOY_HOST} '
docker pull ${IMAGE}:latest
docker rm -f buildflow-web 2>/dev/null || true
docker run -d --name buildflow-web --restart unless-stopped -p 5175:80 $IMAGE:latest
'''
}
docker run -d --name buildflow-web --restart unless-stopped -p ${HOST_PORT}:${APP_PORT} ${IMAGE}:latest
'
"""
echo "部署完成"
} catch (e) {
echo "部署节点暂不可用,跳过自动部署。镜像已推送 Harbor。"
echo "警告:部署节点 ${DEPLOY_HOST} 暂不可用,跳过自动部署(镜像已推送 Harbor)。原因:${e.getMessage()}"
}
}
}
}
stage("HealthCheck") {
steps {
script {
try {
sh """
sleep 5
ssh -o StrictHostKeyChecking=accept-new ${DEPLOY_HOST} 'wget -q --spider http://127.0.0.1:${HOST_PORT}/ && echo HEALTHCHECK_OK'
"""
echo "=== HealthCheck OK: web http://110.42.240.10:${HOST_PORT} ==="
} catch (e) {
echo "警告:HealthCheck 探测失败(容器可能仍在启动)。镜像已推送 Harbor,可手动检查。"
}
}
}
}
}
post {
success {
script { feishuNotify("SUCCESS") }
}
failure {
script { feishuNotify("FAILURE", "构建失败,请查看日志") }
}
always {
success { echo '=== buildflow-web CI/CD SUCCESS ===' }
failure { echo '=== buildflow-web CI/CD FAILED ===' }
always {
sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest 2>/dev/null || true"
deleteDir()
}

View File

@ -0,0 +1,38 @@
# BuildFlow Web (web-antdv-next) Nginx 运行配置
# 仅 server 块,作为 conf.d/default.conf 覆盖 nginx 默认站点
# - SPA history 路由回退 index.html
# - /api 反代到后端(生产由部署侧决定是否启用)
# - 静态资源长缓存
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/json application/xml
application/rss+xml image/svg+xml font/woff font/woff2;
# SPA history fallback
location / {
try_files $uri $uri/ /index.html;
}
# Vite 产物带 hash 文件名,可安全 immutable 长缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}