From cb06599dca0d222ea1fcfa1ba7f07cb938a9271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 24 Jul 2026 19:03:08 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E9=83=A8=E7=BD=B2=E5=A5=91=E7=BA=A6=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加生产构建与 Nginx 镜像冒烟命令 - 校验根路径跳转、静态资源前缀和错误分享地址 --- README.md | 7 ++ easyflow-ui-admin/package.json | 2 + .../scripts/deploy/Dockerfile.smoke | 12 +++ .../deploy/Dockerfile.smoke.dockerignore | 7 ++ .../scripts/deploy/smoke-test.mjs | 92 +++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 easyflow-ui-admin/scripts/deploy/Dockerfile.smoke create mode 100644 easyflow-ui-admin/scripts/deploy/Dockerfile.smoke.dockerignore create mode 100644 easyflow-ui-admin/scripts/deploy/smoke-test.mjs diff --git a/README.md b/README.md index 23da2ffd..5cee27aa 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,13 @@ pnpm dev 默认测试账号:`admin / Easy@2026` +管理端发布前可执行以下命令,验证环境契约、生产镜像和 Nginx 路由: + +```bash +cd easyflow-ui-admin +pnpm verify:deployment +``` + ## 后端 Jar 包构建与部署 ### 构建 Jar diff --git a/easyflow-ui-admin/package.json b/easyflow-ui-admin/package.json index d630db25..3c85f123 100644 --- a/easyflow-ui-admin/package.json +++ b/easyflow-ui-admin/package.json @@ -30,9 +30,11 @@ "preview": "turbo-run preview", "publint": "vsh publint", "reinstall": "pnpm clean --del-lock && pnpm install", + "test:deployment-contract": "vitest run --dom app/vite-base-path-redirect.test.ts app/src/startup-error.test.ts app/src/router/__tests__/environment-contract.test.ts app/src/router/navigation-user-info.test.ts app/src/utils/share-route-context.test.ts app/src/utils/__tests__/login-redirect.test.ts app/src/views/ai/workflow/workflow-share-context.test.ts", "test:unit": "vitest run --dom", "test:e2e": "turbo run test:e2e", "update:deps": "npx taze -r -w", + "verify:deployment": "pnpm run test:deployment-contract && pnpm run build:app && docker build --platform linux/amd64 -f scripts/deploy/Dockerfile.smoke -t easyflow-frontend-smoke:local . && cross-env EASYFLOW_FRONTEND_IMAGE=easyflow-frontend-smoke:local node scripts/deploy/smoke-test.mjs", "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile", "catalog": "pnpx codemod pnpm/catalog" }, diff --git a/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke b/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke new file mode 100644 index 00000000..57ac023a --- /dev/null +++ b/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke @@ -0,0 +1,12 @@ +FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:stable-alpine + +RUN rm -f /etc/nginx/conf.d/default.conf + +COPY app/dist/ /usr/share/nginx/html/flow/ +COPY scripts/deploy/nginx.conf /etc/nginx/nginx.conf + +RUN nginx -t + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke.dockerignore b/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke.dockerignore new file mode 100644 index 00000000..2b04591d --- /dev/null +++ b/easyflow-ui-admin/scripts/deploy/Dockerfile.smoke.dockerignore @@ -0,0 +1,7 @@ +** +!app/ +!app/dist/ +!app/dist/** +!scripts/ +!scripts/deploy/ +!scripts/deploy/nginx.conf diff --git a/easyflow-ui-admin/scripts/deploy/smoke-test.mjs b/easyflow-ui-admin/scripts/deploy/smoke-test.mjs new file mode 100644 index 00000000..43f5250c --- /dev/null +++ b/easyflow-ui-admin/scripts/deploy/smoke-test.mjs @@ -0,0 +1,92 @@ +import { execFileSync } from 'node:child_process'; + +const image = process.env.EASYFLOW_FRONTEND_IMAGE || 'easyflow-frontend:0.1'; +const containerName = `easyflow-frontend-smoke-${process.pid}`; + +function docker(args) { + return execFileSync('docker', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); +} + +async function waitForServer(origin) { + let lastError; + for (let attempt = 0; attempt < 40; attempt += 1) { + try { + const response = await fetch(`${origin}/flow/`); + if (response.ok) { + return; + } + lastError = new Error(`HTTP ${response.status}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Nginx 未在预期时间内启动: ${String(lastError)}`); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +try { + docker([ + 'run', + '--detach', + '--rm', + '--publish', + '127.0.0.1::80', + '--name', + containerName, + image, + ]); + + const portBinding = docker(['port', containerName, '80/tcp']) + .split('\n') + .find((line) => line.startsWith('127.0.0.1:')); + const port = portBinding?.match(/:(\d+)$/)?.[1]; + assert(port, `无法解析容器端口: ${portBinding || 'empty'}`); + + const origin = `http://127.0.0.1:${port}`; + await waitForServer(origin); + + const rootResponse = await fetch(`${origin}/`, { redirect: 'manual' }); + assert(rootResponse.status === 302, `根路径状态异常: ${rootResponse.status}`); + const rootLocation = rootResponse.headers.get('location'); + assert( + rootLocation && new URL(rootLocation, origin).pathname === '/flow/', + `根路径重定向异常: ${rootLocation}`, + ); + + const flowResponse = await fetch(`${origin}/flow/`); + const indexHtml = await flowResponse.text(); + assert(flowResponse.ok, `/flow/ 状态异常: ${flowResponse.status}`); + assert(indexHtml.includes('
'), '/flow/ 未返回应用入口'); + + const assetPath = indexHtml.match(/(?:src|href)="(\/flow\/[^"]+)"/)?.[1]; + assert(assetPath, '构建产物未使用 /flow/ 静态资源前缀'); + const assetResponse = await fetch(`${origin}${assetPath}`); + assert(assetResponse.ok, `静态资源状态异常: ${assetResponse.status}`); + + const invalidRootShareResponse = await fetch( + `${origin}/share/knowledge?shareKey=smoke-test`, + ); + assert( + invalidRootShareResponse.status === 404, + `根路径分享地址应返回 404,实际为 ${invalidRootShareResponse.status}`, + ); + + console.log( + '部署冒烟通过:根路径重定向、/flow/ 入口、静态资源前缀和错误分享路径均符合预期。', + ); +} finally { + try { + docker(['rm', '--force', containerName]); + } catch { + // 容器可能因 --rm 已自动删除。 + } +}