比较提交
2 次代码提交
d5b6d76e60
...
c56eb1abff
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
c56eb1abff | ||
|
|
c3dfb5eddb |
@ -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
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
普通文件
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: {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户