Compare commits

...

10 Commits

Author SHA1 Message Date
xiangning
edfb8aedba chore(deploy): standard base images + Jenkinsfile for 154 CI/CD 2026-10-25 14:18:11 +08:00
xiangning
37d7e06d38 feat(nginx): 安全响应头加到 location 块(防 nginx add_header 继承覆盖)
nginx 的 add_header 在 location 有自己的 add_header 时不继承 server 块,
portal 的 location / 和 location = /index.html 都有 Cache-Control,
需在两处重复加安全头:X-Frame-Options/X-Content-Type-Options/
Referrer-Policy/X-XSS-Protection/Permissions-Policy
2026-07-19 14:29:53 +08:00
xiangning
501f70d489 feat(cicd): 接入自动触发 + docker compose 部署
- Jenkinsfile 加 GenericTrigger(token=buildflow-portal)
- 部署 stage 改 docker compose
- HealthCheck 改轮询,失败标 unstable
2026-07-17 15:22:51 +08:00
xiangning
ba3727723e perf(docker): 调大 pnpm fetch 超时与重试,防 install 中断 2026-07-17 13:29:23 +08:00
xiangning
79742d25f8 perf(docker): npm 源走阿里 npmmirror,避免 registry.npmjs.org 不稳
pnpm install 前设 registry=https://registry.npmmirror.com,
解决构建时 npm 包 error(23)/拉取超时。
2026-07-17 13:21:38 +08:00
xiangning
5b34880b11 perf(docker): base 镜像走 Harbor Proxy Cache
node/nginx FROM 改走 dockerhub-proxy,经 Harbor 回源 docker.io 本地缓存,
避免公网拉取超时。
2026-07-17 12:13:03 +08:00
xiangning
b3d8750816 fix(cicd): 部署 stage 加远程 docker login Harbor
Harbor 私有仓库,cd-target pull 需先登录。withCredentials 传凭证
通过 ssh 执行 docker login(stdin 传密码)。
2026-07-17 11:40:38 +08:00
xiangning
cb75e7096a fix(cicd): 部署 SSH 用 IP 直连 + sudo docker
实测 Jenkins 容器内无 cd-target ssh 别名,cd-target docker 需 sudo。
- DEPLOY_HOST: cd-target → ubuntu@110.42.240.10
- 远程命令: docker → sudo docker
2026-07-17 11:36:07 +08:00
xiangning
095346b48e fix(cicd): portal 流水线 - 重写为可用版本(旧模板有致命 bug)
修复 portal 反复 FAILURE 的根因:
- 旧 Jenkinsfile 指向已废弃的 192.168.1.154:31010 registry(非当前 Harbor)
- 旧 Jenkinsfile 用 docker-registry-credentials 凭证(未配置)
- 旧 Jenkinsfile 镜像名写成 buildflow-web(portal 仓库却叫 web,名称错误)
- sh heredoc 内误用 Groovy 风格 // 注释(shell 会把 // 当命令执行报错)

重写为与 api/web 对齐的 clean version:
- Harbor 43.134.235.58:58080 / buildflow/buildflow-portal
- harbor-credentials 凭证
- SSH 直连 cd-target 部署(-p 5173:5173)
- HealthCheck stage
2026-07-17 11:22:52 +08:00
xiangning
29bcbbdb72 fix: clean Jenkinsfile (single-quote sh blocks) 2026-07-03 18:12:29 +08:00
4 changed files with 60 additions and 98 deletions

View File

@ -1,5 +1,6 @@
# 多阶段构建:第一阶段用 Node 构建产物,第二阶段用 Nginx 托管静态资源
# 用 Node 22 LTS:Node 20.19.x 上 corepack/pnpm 现代版有 VM builtin module 兼容问题
# base 镜像走 Harbor Proxy Cache(dockerhub-proxy → docker.io),避免公网拉取超时
FROM node:22-alpine AS builder
WORKDIR /app
@ -8,6 +9,13 @@ WORKDIR /app
# corepack 在 alpine 上即便 prepare 也不可靠 shim 出 pnpm 命令,npm 装最稳妥
RUN npm install -g pnpm@11.9.0
# npm 包源走阿里 npmmirror(国内最稳,避免 registry.npmjs.org 拉取不稳)
# 调大 fetch 超时与重试,防止 install 中断
RUN pnpm config set registry https://registry.npmmirror.com \
&& pnpm config set fetch-timeout 600000 \
&& pnpm config set fetch-retries 5 \
&& pnpm config set fetch-retry-mintimeout 10000
# 先拷贝依赖清单,利用 Docker 层缓存
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

124
Jenkinsfile vendored
View File

@ -1,101 +1,31 @@
// BuildFlow buildflow-portal 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 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/d215e8ed-edea-4f07-a77c-2a0e5b52d9b9 -H "Content-Type: application/json" -d @/tmp/feishu_msg.json || true'
sh 'rm -f /tmp/feishu_msg.json'
}
pipeline {
agent any
environment {
HARBOR = "43.134.235.58:58080"
IMAGE = "${HARBOR}/buildflow/buildflow-portal"
TAG = "${env.BUILD_NUMBER}"
agent any
options { timestamps() }
environment {
IMG = "127.0.0.1/middleware/buildflow-portal"
}
stages {
stage("Build image") {
steps {
sh "docker build -t ${IMG}:latest -t ${IMG}:${BUILD_NUMBER} ."
}
}
options {
timeout(time: 30, unit: "MINUTES")
buildDiscarder(logRotator(numToKeepStr: "10"))
stage("Push to Harbor") {
steps {
withCredentials([usernamePassword(credentialsId: "harbor-admin", usernameVariable: "HU", passwordVariable: "HP")]) {
sh 'echo "$HP" | docker login 127.0.0.1 -u "$HU" --password-stdin'
sh "docker push ${IMG}:latest && docker push ${IMG}:${BUILD_NUMBER}"
}
}
}
stages {
stage("① 拉取代码") {
steps {
checkout scm
sh "git log --oneline -3"
}
}
stage("② 编译打包") {
steps {
sh "docker build -t ${IMAGE}:${TAG} -t ${IMAGE}:latest ."
sh "docker images | grep ${IMAGE} | head -2"
}
}
stage("推送 Harbor") {
steps {
withCredentials([usernamePassword(credentialsId: "harbor-credentials", usernameVariable: "HUSER", passwordVariable: "HPASS")]) {
sh '''
echo $HPASS | docker login $HARBOR -u $HUSER --password-stdin
docker push $IMAGE:$TAG
docker push $IMAGE:latest
docker logout $HARBOR
'''
}
}
}
stage("③ 部署") {
steps {
script {
try {
node("deploy-agent") {
sh '''
docker pull $IMAGE:latest
docker rm -f buildflow-portal 2>/dev/null || true
docker run -d --name buildflow-portal --restart unless-stopped -p 5174:5173 $IMAGE:latest
'''
}
echo "部署完成"
} catch (e) {
echo "部署节点暂不可用,跳过自动部署。镜像已推送 Harbor。"
}
}
}
}
stage("Deploy") {
steps {
sh "docker compose -f /home/xiangning/deploy/buildflow/docker-compose.yml pull buildflow-portal"
sh "docker compose -f /home/xiangning/deploy/buildflow/docker-compose.yml up -d --no-deps buildflow-portal"
}
}
post {
success {
script { feishuNotify("SUCCESS") }
}
failure {
script { feishuNotify("FAILURE", "构建失败,请查看日志") }
}
always {
sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest 2>/dev/null || true"
deleteDir()
}
}
}
}
post {
always { sh "docker image prune -f || true" }
}
}

View File

@ -21,6 +21,18 @@ services:
networks:
- buildflow-net
# KKFileView 在线文档预览服务(Office/PDF/图片等 30+ 格式)
# 后端 file 模块的 /api/files/office-preview 端点生成预览地址指向此服务。
# 本地预览:http://localhost:8012
kkfileview:
image: keking/kkfileview:4.4.0
container_name: buildflow-kkfileview
restart: unless-stopped
ports:
- "8012:8012"
networks:
- buildflow-net
networks:
buildflow-net:
driver: bridge

View File

@ -35,6 +35,7 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# 静态资源长缓存(Vite 产物带 hash 文件名,可安全 immutable)
location /assets/ {
@ -61,14 +62,25 @@ server {
try_files $uri =404;
}
# SPA history 路由回退
# SPA history 路由回退(加安全头,nginx add_header 继承规则:location 有自己的 add_header 时不继承 server)
location / {
try_files $uri $uri/ /index.html;
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;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
# index.html 不缓存,保证发布后立即生效
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
# 安全响应头(同理重复,防继承覆盖)
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;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
expires 0;
}