比较提交

..

2 次代码提交

作者 SHA1 备注 提交日期
XuqmGroup
c56eb1abff ci: use scoped SSH credentials for web version pushes 2026-07-27 12:36:21 +08:00
XuqmGroup
c3dfb5eddb fix(web): retain prior hashed assets across releases 2026-07-27 12:36:21 +08:00
共有 15 个文件被更改,包括 262 次插入38 次删除

查看文件

@ -1,6 +1,24 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
ARG PREVIOUS_IMAGE=nginx:1.27-alpine
ARG SERVICE_VERSION=0.0.0
FROM ${PREVIOUS_IMAGE} AS previous
RUN set -eu; \
mkdir -p /xuqm-previous; \
if [ -f /etc/xuqm_asset_layout_v2 ] && [ -s /etc/service_version ]; then \
previous_version="$(cat /etc/service_version)"; \
source_dir="/usr/share/nginx/html/releases/${previous_version}"; \
if [ -d "${source_dir}" ]; then \
mkdir -p /xuqm-previous/releases; \
cp -a "${source_dir}" /xuqm-previous/releases/; \
fi; \
elif [ -d /usr/share/nginx/html/assets ]; then \
cp -a /usr/share/nginx/html/assets /xuqm-previous/; \
fi
FROM --platform=linux/amd64 node:22-alpine AS build FROM --platform=linux/amd64 node:22-alpine AS build
WORKDIR /workspace WORKDIR /workspace
ARG SERVICE_VERSION
COPY package.json ./package.json COPY package.json ./package.json
COPY yarn.lock ./yarn.lock COPY yarn.lock ./yarn.lock
@ -22,15 +40,19 @@ ARG OPS_API_BASE_URL=/api
RUN cd ops-platform && \ RUN cd ops-platform && \
VITE_APP_BASE=${OPS_APP_BASE} \ VITE_APP_BASE=${OPS_APP_BASE} \
VITE_API_BASE_URL=${OPS_API_BASE_URL} \ VITE_API_BASE_URL=${OPS_API_BASE_URL} \
VITE_RELEASE_ID=${SERVICE_VERSION} \
yarn build yarn build
FROM --platform=linux/amd64 nginx:1.27-alpine FROM --platform=linux/amd64 nginx:1.27-alpine
ARG SERVICE_VERSION=0.0.0 ARG SERVICE_VERSION=0.0.0
RUN echo "${SERVICE_VERSION}" > /etc/service_version
LABEL com.xuqm.version="${SERVICE_VERSION}" LABEL com.xuqm.version="${SERVICE_VERSION}"
LABEL com.xuqm.service="ops-web" LABEL com.xuqm.service="ops-web"
COPY nginx/ops.conf /etc/nginx/conf.d/default.conf COPY nginx/ops.conf /etc/nginx/conf.d/default.conf
COPY --from=previous /xuqm-previous/ /usr/share/nginx/html/
COPY --from=build /workspace/ops-platform/dist /usr/share/nginx/html COPY --from=build /workspace/ops-platform/dist /usr/share/nginx/html
RUN touch /etc/xuqm_asset_layout_v2
EXPOSE 80 EXPOSE 80

查看文件

@ -1,6 +1,31 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
ARG PREVIOUS_IMAGE=nginx:1.27-alpine
ARG SERVICE_VERSION=0.0.0
# 只抽取上一镜像的“当前版本”资源。首次切换新目录协议时额外保留一次旧 /assets,
# 下一次构建会因 layout 标记存在而自动淘汰,避免历史资源无限累积。
FROM ${PREVIOUS_IMAGE} AS previous
RUN set -eu; \
mkdir -p /xuqm-previous/tenant /xuqm-previous/docs; \
if [ -f /etc/xuqm_asset_layout_v2 ] && [ -s /etc/service_version ]; then \
previous_version="$(cat /etc/service_version)"; \
for app in tenant docs; do \
source_dir="/usr/share/nginx/html/${app}/releases/${previous_version}"; \
if [ -d "${source_dir}" ]; then \
mkdir -p "/xuqm-previous/${app}/releases"; \
cp -a "${source_dir}" "/xuqm-previous/${app}/releases/"; \
fi; \
done; \
else \
for app in tenant docs; do \
source_dir="/usr/share/nginx/html/${app}/assets"; \
if [ -d "${source_dir}" ]; then cp -a "${source_dir}" "/xuqm-previous/${app}/"; fi; \
done; \
fi
FROM --platform=linux/amd64 node:22-alpine AS build FROM --platform=linux/amd64 node:22-alpine AS build
WORKDIR /workspace WORKDIR /workspace
ARG SERVICE_VERSION
COPY package.json ./package.json COPY package.json ./package.json
COPY yarn.lock ./yarn.lock COPY yarn.lock ./yarn.lock
@ -23,20 +48,21 @@ ARG TENANT_API_BASE_URL=/api
RUN cd tenant-platform && \ RUN cd tenant-platform && \
VITE_APP_BASE=${TENANT_APP_BASE} \ VITE_APP_BASE=${TENANT_APP_BASE} \
VITE_API_BASE_URL=${TENANT_API_BASE_URL} \ VITE_API_BASE_URL=${TENANT_API_BASE_URL} \
VITE_RELEASE_ID=${SERVICE_VERSION} \
yarn build yarn build
RUN cd docs-site && yarn build RUN cd docs-site && VITE_RELEASE_ID=${SERVICE_VERSION} yarn build
FROM --platform=linux/amd64 nginx:1.27-alpine FROM --platform=linux/amd64 nginx:1.27-alpine
ARG SERVICE_VERSION=0.0.0 ARG SERVICE_VERSION=0.0.0
# RUN 写文件确保 SERVICE_VERSION 变化时 BuildKit inline cache 失效LABEL 单独不可靠)
RUN echo "${SERVICE_VERSION}" > /etc/service_version
LABEL com.xuqm.version="${SERVICE_VERSION}" LABEL com.xuqm.version="${SERVICE_VERSION}"
LABEL com.xuqm.service="tenant-web" LABEL com.xuqm.service="tenant-web"
COPY nginx/tenant.conf /etc/nginx/conf.d/default.conf COPY nginx/tenant.conf /etc/nginx/conf.d/default.conf
COPY --from=previous /xuqm-previous/ /usr/share/nginx/html/
COPY --from=build /workspace/tenant-platform/dist /usr/share/nginx/html/tenant COPY --from=build /workspace/tenant-platform/dist /usr/share/nginx/html/tenant
COPY --from=build /workspace/docs-site/docs/.vitepress/dist /usr/share/nginx/html/docs COPY --from=build /workspace/docs-site/docs/.vitepress/dist /usr/share/nginx/html/docs
RUN echo "${SERVICE_VERSION}" > /etc/service_version && touch /etc/xuqm_asset_layout_v2
EXPOSE 80 EXPOSE 80

7
Jenkinsfile vendored
查看文件

@ -88,8 +88,9 @@ pipeline {
chcp 65001 >nul chcp 65001 >nul
docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS%
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker pull --platform=linux/amd64 ${latestImage} || echo Pull failed, will build fresh set "PREVIOUS_IMAGE=nginx:1.27-alpine"
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg GIT_COMMIT=${gitCommit} --build-arg SERVICE_VERSION=${svcVersion} ${cacheArgs} -t ${versionedImage} -t ${latestImage} . docker pull --platform=linux/amd64 ${latestImage} && set "PREVIOUS_IMAGE=${latestImage}" || echo Previous image unavailable, first release will not retain assets
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg PREVIOUS_IMAGE=%PREVIOUS_IMAGE% --build-arg GIT_COMMIT=${gitCommit} --build-arg SERVICE_VERSION=${svcVersion} ${cacheArgs} -t ${versionedImage} -t ${latestImage} .
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker push ${versionedImage} docker push ${versionedImage}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
@ -147,6 +148,7 @@ print('versions.json updated: ${svcName}=${svcVer}')
stage('Commit Version') { stage('Commit Version') {
steps { steps {
sshagent(credentials: ['jenkins-ssh-key']) {
bat """ bat """
chcp 65001 >nul chcp 65001 >nul
git config user.email "jenkins@xuqm.com" git config user.email "jenkins@xuqm.com"
@ -158,6 +160,7 @@ print('versions.json updated: ${svcName}=${svcVer}')
} }
} }
} }
}
post { post {
success { bat "chcp 65001 >nul && echo ✅ ${env.IMAGE_NAME}:${env.SERVICE_VERSION} 构建部署成功" } success { bat "chcp 65001 >nul && echo ✅ ${env.IMAGE_NAME}:${env.SERVICE_VERSION} 构建部署成功" }

查看文件

@ -60,8 +60,9 @@ pipeline {
bat """ bat """
docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS%
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker pull --platform=linux/amd64 ${latestImage} || echo Pull failed, will build fresh set "PREVIOUS_IMAGE=nginx:1.27-alpine"
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg GIT_COMMIT=${gitCommit} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} . docker pull --platform=linux/amd64 ${latestImage} && set "PREVIOUS_IMAGE=${latestImage}" || echo Previous image unavailable, first release will not retain assets
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg PREVIOUS_IMAGE=%PREVIOUS_IMAGE% --build-arg GIT_COMMIT=${gitCommit} --build-arg SERVICE_VERSION=${env.IMAGE_VERSION} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} .
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker push ${versionedImage} docker push ${versionedImage}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
@ -109,6 +110,7 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
stage('Commit Version') { stage('Commit Version') {
steps { steps {
sshagent(credentials: ['jenkins-ssh-key']) {
script { script {
bat """ bat """
git config user.email "jenkins@xuqm.com" git config user.email "jenkins@xuqm.com"
@ -121,6 +123,7 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
} }
} }
} }
}
post { post {
success { bat "chcp 65001 >nul && echo ✅ ${env.IMAGE_NAME}:${env.IMAGE_VERSION} 构建部署成功" } success { bat "chcp 65001 >nul && echo ✅ ${env.IMAGE_NAME}:${env.IMAGE_VERSION} 构建部署成功" }

查看文件

@ -39,8 +39,8 @@ pipeline {
branches: [[name: 'main']], branches: [[name: 'main']],
extensions: [[$class: 'CleanBeforeCheckout']], extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: [[ userRemoteConfigs: [[
url: 'https://xuqinmin.com/xuqmGroup/XuqmGroup-Web.git', url: 'ssh://git@xuqinmin.com:2222/xuqmGroup/XuqmGroup-Web.git',
credentialsId: 'XUQM_GIT_CREDENTIALS' credentialsId: 'jenkins-ssh-key'
]] ]]
]) ])
} }
@ -90,8 +90,9 @@ pipeline {
bat """ bat """
docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS%
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker pull --platform=linux/amd64 ${latestImage} || echo Pull failed, will build fresh set "PREVIOUS_IMAGE=nginx:1.27-alpine"
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg GIT_COMMIT=${gitCommit} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} . docker pull --platform=linux/amd64 ${latestImage} && set "PREVIOUS_IMAGE=${latestImage}" || echo Previous image unavailable, first release will not retain assets
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg PREVIOUS_IMAGE=%PREVIOUS_IMAGE% --build-arg GIT_COMMIT=${gitCommit} --build-arg SERVICE_VERSION=${env.IMAGE_VERSION} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} .
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker push ${versionedImage} docker push ${versionedImage}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
@ -141,6 +142,7 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
stage('Commit Version') { stage('Commit Version') {
when { expression { params.DEPLOY_ENV == 'production' } } when { expression { params.DEPLOY_ENV == 'production' } }
steps { steps {
sshagent(credentials: ['jenkins-ssh-key']) {
script { script {
bat """ bat """
git config user.email "jenkins@xuqm.com" git config user.email "jenkins@xuqm.com"
@ -153,6 +155,7 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
} }
} }
} }
}
post { post {
success { success {

查看文件

@ -1,10 +1,16 @@
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
const releaseId = process.env.VITE_RELEASE_ID?.trim()
if (releaseId && !/^[A-Za-z0-9._-]+$/.test(releaseId)) {
throw new Error('VITE_RELEASE_ID 只能包含字母、数字、点、下划线和短横线')
}
export default defineConfig({ export default defineConfig({
title: 'XuqmGroup 开发者文档', title: 'XuqmGroup 开发者文档',
description: 'XuqmGroup 多端 SDK 接入指南', description: 'XuqmGroup 多端 SDK 接入指南',
base: '/docs/', base: '/docs/',
lang: 'zh-CN', lang: 'zh-CN',
assetsDir: releaseId ? `releases/${releaseId}/assets` : 'assets',
themeConfig: { themeConfig: {
logo: '/logo.svg', logo: '/logo.svg',

查看文件

@ -11,6 +11,12 @@ SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOF
- 已删除废弃的离线 license 服务页面、路由、API、菜单和日志入口。 - 已删除废弃的离线 license 服务页面、路由、API、菜单和日志入口。
- 依赖管理统一为根 `package.json` 声明的 Yarn 1 与 `yarn.lock` - 依赖管理统一为根 `package.json` 声明的 Yarn 1 与 `yarn.lock`
- `Jenkinsfile.tenant-web` 默认 development,仅验证;production 需要人工确认才允许推送、部署和提交版本号。 - `Jenkinsfile.tenant-web` 默认 development,仅验证;production 需要人工确认才允许推送、部署和提交版本号。
- Web 生产缓存使用版本目录协议:入口 HTML/manifest 禁止持久缓存,哈希资源使用
`releases/<SERVICE_VERSION>/assets/` 和一年 `immutable`
- Jenkins 构建会把上一 `latest` 镜像作为 `PREVIOUS_IMAGE`;新镜像只保留上一版本的
当前资源目录和本次资源目录。首次从旧布局迁移时保留一次旧 `/assets`,下一版本自动
淘汰,避免资源无限累积。
- 哈希资源 location 使用 `try_files $uri =404`,不得回退到 SPA `index.html`
验证: 验证:
@ -19,8 +25,19 @@ yarn install --frozen-lockfile
yarn build:tenant yarn build:tenant
yarn build:ops yarn build:ops
yarn workspace @xuqm/docs-site build yarn workspace @xuqm/docs-site build
yarn test:web-cache
nginx -t -p "$PWD/" -c scripts/nginx-test.conf
``` ```
2026-07-26 三项构建均已通过。 2026-07-27 缓存契约测试会使用固定 release ID 重新构建 tenant、ops 与 docs,并校验
HTML 引用、版本目录、Nginx 缓存头及 Docker 上一版本保留门禁。
未执行提交、推送、Jenkins 或部署。 Jenkins 和线上部署需由发布流程另行执行;本地构建不能代替生产响应头验收。
# 2026-07-27 Jenkins SCM 凭据收口
- tenant-web 显式 checkout 已改用 Gitea SSH URL 和 Jenkins
`jenkins-ssh-key`,不再引用未配置的 `XUQM_GIT_CREDENTIALS`;其它 Web 作业继续
复用各自 Jenkins SCM 配置。三套 Web 流水线的版本文件回写均使用
`sshagent(jenkins-ssh-key)`,避免 checkout 结束后 push 缺少凭据。
- 该修改尚未通过下一次 Web Jenkins 构建验证;缓存策略提交部署前必须先确认
checkout 与三套前端构建均成功。

查看文件

@ -5,17 +5,37 @@ server {
root /usr/share/nginx/html/tenant; root /usr/share/nginx/html/tenant;
index index.html; index index.html;
location ~* ^/(?:releases/[^/]+/assets|assets)/ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* ^/ops/(?:releases/[^/]+/assets|assets)/ {
root /usr/share/nginx/html;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* ^/docs/(?:releases/[^/]+/assets|assets)/ {
root /usr/share/nginx/html;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
} }
location /ops/ { location /ops/ {
alias /usr/share/nginx/html/ops/; alias /usr/share/nginx/html/ops/;
try_files $uri $uri/ /ops/index.html; try_files $uri $uri/ /ops/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
} }
location /docs/ { location /docs/ {
alias /usr/share/nginx/html/docs/; alias /usr/share/nginx/html/docs/;
try_files $uri $uri/ /docs/index.html; try_files $uri $uri/ /docs/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
} }
} }

查看文件

@ -5,7 +5,15 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
location ~* ^/(?:releases/[^/]+/assets|assets)/ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
} }
} }

查看文件

@ -5,12 +5,31 @@ server {
root /usr/share/nginx/html/tenant; root /usr/share/nginx/html/tenant;
index index.html; index index.html;
# Vite 哈希资源属于不可变内容。独立 location 必须返回真实 404,禁止回退到
# index.html,否则 chunk 丢失会被伪装成 MIME/语法错误。
location ~* ^/(?:releases/[^/]+/assets|assets)/ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* ^/docs/(?:releases/[^/]+/assets|assets)/ {
root /usr/share/nginx/html;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
} }
location /docs/ { location /docs/ {
alias /usr/share/nginx/html/docs/; alias /usr/share/nginx/html/docs/;
try_files $uri $uri/ /docs/index.html; try_files $uri $uri/ /docs/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
} }
} }

查看文件

@ -6,6 +6,10 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '') const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '')
const releaseId = env.VITE_RELEASE_ID?.trim()
if (releaseId && !/^[A-Za-z0-9._-]+$/.test(releaseId)) {
throw new Error('VITE_RELEASE_ID 只能包含字母、数字、点、下划线和短横线')
}
return { return {
base: env.VITE_APP_BASE || '/', base: env.VITE_APP_BASE || '/',
@ -18,6 +22,7 @@ export default defineConfig(({ mode }) => {
alias: { '@': new URL('./src', import.meta.url).pathname }, alias: { '@': new URL('./src', import.meta.url).pathname },
}, },
build: { build: {
assetsDir: releaseId ? `releases/${releaseId}/assets` : 'assets',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { rollupOptions: {
output: { output: {

查看文件

@ -10,9 +10,11 @@
"scripts": { "scripts": {
"dev:tenant": "yarn workspace tenant-platform dev", "dev:tenant": "yarn workspace tenant-platform dev",
"dev:ops": "yarn workspace ops-platform dev", "dev:ops": "yarn workspace ops-platform dev",
"dev:docs": "yarn workspace docs-site dev", "dev:docs": "yarn workspace @xuqm/docs-site dev",
"build:tenant": "yarn workspace tenant-platform build", "build:tenant": "yarn workspace tenant-platform build",
"build:ops": "yarn workspace ops-platform build", "build:ops": "yarn workspace ops-platform build",
"build:docs": "yarn workspace @xuqm/docs-site build",
"test:web-cache": "node scripts/verify-web-cache-contract.mjs",
"build": "yarn workspaces run build" "build": "yarn workspaces run build"
}, },
"devDependencies": { "devDependencies": {

10
scripts/nginx-test.conf 普通文件
查看文件

@ -0,0 +1,10 @@
pid /tmp/xuqm-web-nginx.pid;
error_log stderr;
events {}
http {
include ../nginx/tenant.conf;
include ../nginx/ops.conf;
include ../nginx/default.conf;
}

查看文件

@ -0,0 +1,74 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
const root = path.resolve(import.meta.dirname, '..')
const releaseId = 'cache-contract-test'
const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
function run(args) {
const result = spawnSync(yarn, args, {
cwd: root,
env: { ...process.env, VITE_RELEASE_ID: releaseId },
stdio: 'inherit',
})
if (result.status !== 0) process.exit(result.status ?? 1)
}
function files(directory, suffix) {
return readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const current = path.join(directory, entry.name)
if (entry.isDirectory()) return files(current, suffix)
return entry.name.endsWith(suffix) ? [current] : []
})
}
function requireText(file, values) {
const content = readFileSync(file, 'utf8')
for (const value of values) {
if (!content.includes(value)) {
throw new Error(`${path.relative(root, file)} 缺少缓存契约:${value}`)
}
}
}
function verifyBuild(output, publicPrefix) {
const assetDirectory = path.join(output, 'releases', releaseId, 'assets')
if (!existsSync(assetDirectory) || !statSync(assetDirectory).isDirectory()) {
throw new Error(`${path.relative(root, output)} 未生成版本化 assets 目录`)
}
const html = files(output, '.html').map(file => readFileSync(file, 'utf8')).join('\n')
const expected = `${publicPrefix}releases/${releaseId}/assets/`
if (!html.includes(expected)) {
throw new Error(`${path.relative(root, output)} 的 HTML 未引用 ${expected}`)
}
}
run(['workspace', 'tenant-platform', 'build'])
run(['workspace', 'ops-platform', 'build'])
run(['workspace', '@xuqm/docs-site', 'build'])
verifyBuild(path.join(root, 'tenant-platform', 'dist'), '/')
verifyBuild(path.join(root, 'ops-platform', 'dist'), '/')
verifyBuild(path.join(root, 'docs-site', 'docs', '.vitepress', 'dist'), '/docs/')
for (const nginx of ['nginx/tenant.conf', 'nginx/ops.conf']) {
requireText(path.join(root, nginx), [
'no-cache, no-store, must-revalidate',
'public, max-age=31536000, immutable',
'try_files $uri =404',
])
}
for (const dockerfile of ['Dockerfile.tenant', 'Dockerfile.ops']) {
requireText(path.join(root, dockerfile), [
'ARG PREVIOUS_IMAGE=',
'/etc/xuqm_asset_layout_v2',
'COPY --from=previous',
'VITE_RELEASE_ID=${SERVICE_VERSION}',
])
}
console.log('Web 缓存与上一版本资源保留契约验证通过')

查看文件

@ -6,6 +6,10 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '') const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '')
const releaseId = env.VITE_RELEASE_ID?.trim()
if (releaseId && !/^[A-Za-z0-9._-]+$/.test(releaseId)) {
throw new Error('VITE_RELEASE_ID 只能包含字母、数字、点、下划线和短横线')
}
return { return {
base: env.VITE_APP_BASE || '/', base: env.VITE_APP_BASE || '/',
@ -24,6 +28,8 @@ export default defineConfig(({ mode }) => {
}, },
}, },
build: { build: {
// 版本目录让新旧 HTML 各自引用不可变资源;容器镜像会同时保留当前和上一版本目录。
assetsDir: releaseId ? `releases/${releaseId}/assets` : 'assets',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { rollupOptions: {
output: { output: {