从 0 到 1 搭建 Jenkins 全自动交付流水线:一套脚本打遍多客户、多平台
你是否也经历过这样的发版夜:
- 前端
dist手动pnpm build,后端手动dotnet publish,再手工拼 zip;- 客户 A 要 Windows、客户 B 要麒麟 ARM、客户 C 要统信 x64,每次都得重来一遍;
- 打包到一半报个错,连锁炸出十几条「系统找不到指定的路径」;
- 包打好了,还得远程桌面登服务器去拷文件。
这篇教程带你用 Jenkins + 一套参数化构建脚本,把上面这一切压缩成「填 5 个参数 → 点构建 → 浏览器下载 zip」,20~30 分钟自动出包。所有步骤都来自真实落地经验,只讲流程、不谈业务,照着抄就能用。
目录
- 我们要解决什么问题
- 整体架构与最终产物
- 第一章:环境与前置准备(一次性)
- 第二章:从 0 安装并配置 Jenkins 任务
- 第三章:核心——参数化构建脚本全公开
- 第四章:踩坑实录(避坑必看)
- 第五章:多客户 / 多平台(异构)怎么打
- 第六章:交付包发版前核验清单
- 结语
1. 我们要解决什么问题
一个前端(Vue3 + Vite + pnpm)+ 后端(.NET 8)的移动端 H5 项目,要交付给多家客户,且每家客户的操作系统与 CPU 架构都不同(普通 Windows Server、麒麟/统信的 x64 与 ARM64 等)。
核心矛盾有三点:
- 多客户差异化:不同客户前端标题/底座不同,但不能每来一个客户就改业务代码;
- 多平台异构:.NET 要按目标平台发布成自包含(self-contained)包,错一个 RID 客户机就跑不起来;
- 可重复 & 可下载:运维/实施同学不在服务器旁边,也要能随时拉到一个结构正确、内容完整的发版包。
解法就是:用 Jenkins 把「克隆 → 构建 → 推送 → 打包 → 归档下载」串成一条参数化流水线。
2. 整体架构与最终产物
Jenkins 任务 delivery-package 一键完成以下阶段:
- 克隆交付仓库(内含固定文件:启停脚本、nginx 部署包、部署文档);
- 前端构建:克隆前端仓库 →
pnpm install→pnpm run build:<客户>; - 后端构建:克隆后端仓库 →
dotnet publish(self-contained,按目标平台); - 把
dist+H5api推送到交付仓库(固定分支); - 拉最新仓库内容 → 打 zip → 复制到 workspace 供归档下载。
最终产物:
H5_XC_<版本>_<平台>_<日期>.zip
包内结构(发版约定,务必固定):
H5_XC_1.0.0_linux-arm64_20260721.zip
├── dist/ # 前端构建产物
├── H5api/
│ ├── api/ # 后端 self-contained 发布产物
│ │ ├── <YourApiProject> # Linux 下无 .exe 后缀的可执行文件
│ │ ├── appsettings.json
│ │ └── *.so # 自带 .NET 运行时
│ ├── start-api.sh / .bat # 启停脚本(按平台复制)
│ ├── stop-api.sh / .bat
│ └── restart-api.sh / .bat
├── nginx部署包/
└── 部署文档.docx
命名规则:
H5_XC_<版本>_<平台>_<日期>.zip,平台如linux-arm64/win-x64。结构一旦定下,客户机的解压即部署脚本就稳定了。
3. 第一章:环境与前置准备(一次性)
3.1 服务器环境
| 组件 | 说明 |
|---|---|
| 操作系统 | Windows Server(Jenkins / 后端 / Nginx 同机) |
| Jenkins | WAR 包启动,路径如 <JENKINS_DIR>\jenkins.war |
| Java | JDK 17(Eclipse Adoptium 等发行版均可) |
| .NET | SDK 8.0.x(后端用) |
| Nginx | 反向代理 + 静态资源,子配置如 conf/conf.d/h5.conf |
| Node / pnpm | 前端构建,pnpm 建议用 npx pnpm@<版本> 锁定 |
启动 Jenkins:
java -jar <JENKINS_DIR>\jenkins.war --httpPort=8080
重启 Nginx:
<NGINX_DIR>\nginx.exe -s reload -p <NGINX_DIR>
端口规划建议:本地自研环境用一组端口(前端 8081 / 后端 8001、8082 / 8002…),发版/交付用另一组(如 8083 / 8003 起,客户端口依次顺延 8084/8004…)。建表记下来,别凭记忆。
3.2 交付仓库结构(固定文件单独管理)
把"不常变"的东西放进一个 Git 仓库(下文称交付仓库),只放固定文件,构建产物由 Jenkins 运行时推送进去,最后统一拉最新打 zip:
delivery-repo/
├── nginx部署包/ # 固定,更新时手动 push
├── 部署文档.docx # 固定,更新时手动 push
├── scripts/ # 固定:启停脚本(start/stop/restart × .sh/.bat 共 6 个)
└── .gitignore # 只写 *.zip
⚠️
.gitignore只能忽略*.zip,dist/、H5api/、scripts/必须被追踪。否则git add -A不会提交它们,最后拉下来打包是空的。
3.3 前端"配置化打包"(多客户差异化,不碰业务代码)
每个客户只需改源码一次,之后全靠参数驱动:
- 新建
.env.<客户名>(与.env.dev同级),变量以VITE_开头: VITE_BASE_API=/api VITE_BASE_ROUTE_URL=/ VITE_CUSTOM_TITLE=客户A VITE_CUSTOM_CODE=customerA package.json的scripts加一行:"build:customerA": "vite build --mode customerA"- 把
.env.customerA和package.json的改动 commit 并 push 到对应分支。
之后 Jenkins 构建脚本用
pnpm run build:customerA,部署到html/h5-customerA,Nginx 加一个 server 块即可。纯配置驱动,未来加客户不用改动流水线。
3.4 启停脚本统一管理
6 个脚本(start/stop/restart-api × .sh/.bat)提交到交付仓库的 scripts/。构建时脚本按目标平台自动选 .sh(Linux) 或 .bat(Windows) 复制进 H5api/ 根目录,不再依赖服务器本地文件。
⚠️
.sh脚本不要靠构建机的echo生成——Windows 的 cmd 是 GBK,写中文/制表符会乱码。放进 Git 仓库用xcopy拷进包最稳。
4. 第二章:从 0 安装并配置 Jenkins 任务
4.1 新建任务
- 新建 Freestyle project;
- 勾选 This project is parameterized(参数化构建);
- 勾选 Delete workspace before build starts(每次清空工作区,避免脏产物)。
4.2 参数设计
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
CUSTOMER | Choice | 客户名(决定 env 文件 + build 脚本) | customerA / customerB |
TARGET_RID | Choice | 目标平台 RID | win-x64 / linux-arm64 |
VERSION | String | 版本号 | 1.0.0 |
FRONTEND_BRANCH | String | 前端分支(自动去 origin/ 前缀) | dev_1.0.0 |
BACKEND_BRANCH | String | 后端分支(自动去前缀) | release_1.0.0 |
前后端分支分开传,是因为实际中两端发版分支经常不一致(比如前端带下划线、后端不带)。脚本开头统一做"去
origin/前缀"处理,填origin/dev_1.0.0也兼容。
4.3 构建步骤
增加一个 Execute Windows batch command,把第三章的完整脚本整段贴进去。
4.4 构建后归档(浏览器直接下载,不用登服务器)
- 任务 → 配置 → 滚到最底部「构建后操作」;
- 点「增加构建后操作步骤」→ 选 Archive the artifacts;
- Files to archive 填:
H5_XC_*.zip; - (可选)勾选 Fingerprint all archived artifacts(便于比对包是否一致)。
构建完成后:进入该次构建页 → 左侧「工件 / Artifacts」→ 点 zip 文件名直接下载。只要浏览器能打开 Jenkins 地址就能取包。
5. 第三章:核心——参数化构建脚本全公开
下面是可直接粘贴的脱敏模板。把 <...> 占位符换成你自己的值即可。<GIT_USER>:<GIT_TOKEN> 是访问令牌,建议用只读部署账号,不要放个人密码。
@echo off
setlocal enabledelayedexpansion
:: 基础环境
set PATH=C:\Program Files\nodejs;C:\Program Files\Git\cmd;%PATH%
git config --global http.sslVerify false
git config --global user.name "jenkins"
git config --global user.email "jenkins@local"
:: ---------- 去掉 Jenkins 传入分支名中的 origin/ 前缀 ----------
set FB=%FRONTEND_BRANCH%
if "%FB:~0,7%"=="origin/" set FB=%FB:~7%
set BB=%BACKEND_BRANCH%
if "%BB:~0,8%"=="origin1/" set BB=%BB:~8%
if "%BB:~0,7%"=="origin/" set BB=%BB:~7%
echo [INFO] 前端分支: %FB%
echo [INFO] 后端分支: %BB%
echo [INFO] 客户: %CUSTOMER% ^| 平台: %TARGET_RID% ^| 版本: %VERSION%
cd /d %WORKSPACE%
:: ==================== 步骤0: 克隆交付仓库 ====================
echo.
echo [步骤0] 克隆交付仓库 delivery-repo ...
if exist delivery-repo rmdir /s /q delivery-repo
git clone http://<GIT_USER>:<GIT_TOKEN>@<GIT_HOST>:<GIT_PORT>/<GROUP>/delivery-repo.git delivery-repo
if errorlevel 1 (
echo [ERROR] delivery-repo 克隆失败,检查仓库地址和权限
exit /b 1
)
:: ==================== 步骤①: 前端构建 ====================
echo.
echo [步骤1] 克隆前端 (分支: %FB%) ...
if exist h5-vue rmdir /s /q h5-vue
git clone -b %FB% http://<GIT_USER>:<GIT_TOKEN>@<GIT_HOST>:<GIT_PORT>/<GROUP>/h5-vue.git h5-vue
if errorlevel 1 (
echo [ERROR] 前端克隆失败,检查分支 %FB% 是否存在
exit /b 1
)
:: ---------- 定制客户白名单:仅白名单客户走 build:客户名,其余走标准版 build:stander ----------
:: 白名单值必须与 Jenkins CUSTOMER 参数的值完全一致(/i 忽略大小写)
set BUILD_SCRIPT=build:stander
if /i "%CUSTOMER%"=="customerA" set BUILD_SCRIPT=build:%CUSTOMER%
if /i "%CUSTOMER%"=="customerB" set BUILD_SCRIPT=build:%CUSTOMER%
echo [INFO] 前端构建命令: pnpm run %BUILD_SCRIPT%
cd h5-vue
call npx pnpm@<PNPM_VERSION> install --no-frozen-lockfile
if errorlevel 1 (
echo [ERROR] pnpm install 失败
exit /b 1
)
call npx pnpm@<PNPM_VERSION> run %BUILD_SCRIPT%
if errorlevel 1 (
echo [ERROR] %BUILD_SCRIPT% 失败!若是定制客户,请确认 package.json 中存在
echo build:%CUSTOMER% 脚本,且 .env.%CUSTOMER% 文件已提交。
exit /b 1
)
echo [OK] 前端构建完成 (%BUILD_SCRIPT%)
:: ==================== 步骤②: 后端构建 ====================
echo.
echo [步骤2] 克隆后端 (分支: %BB%) ...
cd /d %WORKSPACE%
if exist h5-api rmdir /s /q h5-api
git clone -b %BB% http://<GIT_USER>:<GIT_TOKEN>@<GIT_HOST>:<GIT_PORT>/<GROUP>/h5-api.git h5-api
if errorlevel 1 (
echo [ERROR] 后端克隆失败,检查分支 %BB% 是否存在
exit /b 1
)
:: 目标输出目录(首次构建该客户时自动创建,避免"路径不存在")
if not exist <DELIVERY_DIR>\%CUSTOMER% mkdir <DELIVERY_DIR>\%CUSTOMER%
cd h5-api
dotnet publish <API_PROJECT>\<API_PROJECT>.csproj -c Release -r %TARGET_RID% --self-contained true -o <DELIVERY_DIR>\%CUSTOMER%\H5api-tmp
if errorlevel 1 (
echo [ERROR] dotnet publish 失败
exit /b 1
)
echo [OK] 后端发布完成
:: ---- 整理 H5api/api/ 结构 + 复制启停脚本 ----
cd /d <DELIVERY_DIR>\%CUSTOMER%
if exist H5api rmdir /s /q H5api
mkdir H5api\api
xcopy /E /I /Y H5api-tmp\* H5api\api\
if errorlevel 1 (
echo [ERROR] 整理 H5api 目录失败
exit /b 1
)
rmdir /s /q H5api-tmp
:: 按平台选择 .sh(Linux) 或 .bat(Windows) 启停脚本
set PLATEXT=sh
echo %TARGET_RID% | findstr /i "win" >nul && set PLATEXT=bat
echo [INFO] 启停脚本扩展名: %PLATEXT%
xcopy /Y %WORKSPACE%\delivery-repo\scripts\start-api.%PLATEXT% H5api\
xcopy /Y %WORKSPACE%\delivery-repo\scripts\stop-api.%PLATEXT% H5api\
xcopy /Y %WORKSPACE%\delivery-repo\scripts\restart-api.%PLATEXT% H5api\
echo [OK] 启停脚本已复制到 H5api/
:: ==================== 步骤③: 推送到交付仓库 ====================
echo.
echo [步骤3] 合并 dist + H5api 到交付仓库 ...
cd /d %WORKSPACE%
if exist delivery-repo\dist rmdir /s /q delivery-repo\dist
if exist delivery-repo\H5api rmdir /s /q delivery-repo\H5api
xcopy /E /I /Y %WORKSPACE%\h5-vue\dist delivery-repo\dist
if errorlevel 1 (
echo [ERROR] 复制 dist 失败,前端可能未成功构建
exit /b 1
)
xcopy /E /I /Y <DELIVERY_DIR>\%CUSTOMER%\H5api delivery-repo\H5api
if errorlevel 1 (
echo [ERROR] 复制 H5api 失败
exit /b 1
)
cd /d %WORKSPACE%\delivery-repo
git add -A
git commit -m "auto: %CUSTOMER% %TARGET_RID% v%VERSION% %date:~0,4%%date:~5,2%%date:~8,2%"
if errorlevel 1 (
echo [WARN] 没有变更需要提交(或 commit 为空),继续打包...
) else (
git push origin <DELIVERY_BRANCH>
if errorlevel 1 (
echo [ERROR] 推送失败,检查分支权限或网络
exit /b 1
)
)
echo [OK] 已推送到 delivery-repo (<DELIVERY_BRANCH>)
:: ==================== 步骤④: 拉最新打 zip ====================
echo.
echo [步骤4] 拉取最新内容并打包 ...
cd /d %WORKSPACE%
if exist package-temp rmdir /s /q package-temp
mkdir package-temp
git clone --depth 1 http://<GIT_USER>:<GIT_TOKEN>@<GIT_HOST>:<GIT_PORT>/<GROUP>/delivery-repo.git package-temp\src
if errorlevel 1 (
echo [ERROR] 打包克隆失败
exit /b 1
)
:: 去掉 .git 元数据和 scripts/ 源码目录(脚本已在 H5api/ 内)
rmdir /s /q package-temp\src\.git 2>nul
if exist package-temp\src\scripts rmdir /s /q package-temp\src\scripts
if not exist <DELIVERY_DIR>\%CUSTOMER% mkdir <DELIVERY_DIR>\%CUSTOMER%
cd /d %WORKSPACE%\package-temp\src
powershell -Command "Compress-Archive -Path '*' -DestinationPath '<DELIVERY_DIR>\%CUSTOMER%\H5_XC_%VERSION%_%TARGET_RID%_%date:~0,4%%date:~5,2%%date:~8,2%.zip' -Force"
if errorlevel 1 (
echo [ERROR] 打包失败
exit /b 1
)
echo [OK] ZIP 已生成
:: ==================== 步骤⑤: 复制到 workspace 供归档下载 ====================
copy "<DELIVERY_DIR>\%CUSTOMER%\H5_XC_%VERSION%_%TARGET_RID%_%date:~0,4%%date:~5,2%%date:~8,2%.zip" "%WORKSPACE%\"
if errorlevel 1 (
echo [WARN] 复制 zip 到 workspace 失败,可手动从 <DELIVERY_DIR>\%CUSTOMER% 取
)
echo.
echo ============================================
echo 完成: %WORKSPACE%\H5_XC_%VERSION%_%TARGET_RID%_%date:~0,4%%date:~5,2%%date:~8,2%.zip
echo ============================================
exit /b 0
注意:批处理里 Git 地址的
@若出现在用户名中需转义为%%40;用<GIT_TOKEN>令牌方式通常不含@,可忽略。另外 Windows bat 调用npx必须加call,否则后续命令会被截断。
6. 第四章:踩坑实录(避坑必看)
这些都是真金白银踩出来的坑,照做能省你一晚上:
6.1 后端源码硬编码端口 + Windows 端口排除范围
appsettings.json 里 Kestrel 写死了一个端口(如 :8000),而该端口被 Windows 端口排除范围占用,直接启动会报 SocketException (10013)。并且 Kestrel Endpoints 配置优先级高于命令行 --urls,传 --urls 无效。
解法:环境变量注入(不修改源码)。.NET 配置系统会以环境变量覆盖 json 值,层级用 __ 双下划线:
set CommonCfg__UserCode=<YourUserCode>
set CommonCfg__IRHost=<YourIrHost>
set CommonCfg__IRPort=<YourIrPort>
set Kestrel__Endpoints__Http__Url=http://0.0.0.0:<YourPort>
IR 是身份/认证服务,配错会"未找到对应的用户"。这类客户相关配置不要写死在脚本里——放包内
appsettings.json让客户按环境改,或按客户预填配置文件随包交付。
6.2 pnpm lockfile 报错(团队规范:不删 lock)
CI 环境 pnpm 默认 frozen-lockfile=true,若 pnpm-lock.yaml 与 package.json 对不上(有人加依赖但没更新 lock),报 ERR_PNPM_OUTDATED_LOCKFILE。
- ❌ 旧做法:
del pnpm-lock.yaml让 pnpm 重新生成(团队不让删); - ✅ 推荐做法:install 加
--no-frozen-lockfile,允许自动更新 lock、不删文件; - ✅ 最规范:本地
pnpm install更新后把新pnpm-lock.yaml提交到 git。
call npx pnpm@<PNPM_VERSION> install --no-frozen-lockfile
6.3 echo 里的竖线 | 必须转义
批处理里想 echo 出带 | 的内容(如 客户 | 平台),必须写成 ^|,否则被当成管道符报错 '平台' 不是内部或外部命令:
echo 客户 ^| 平台
6.4 分支前缀 origin/
Jenkins Git 插件传入的分支常带 origin/ 前缀,git clone -b origin/dev 会报 Remote branch origin/xxx not found。脚本开头已统一去前缀(见第三章步骤0),新写脚本务必带上。
6.5 客户目录不存在
首次构建某客户时 <DELIVERY_DIR>\<CUSTOMER> 还没创建,publish/打包会报"路径不存在"。在 publish 前加:
if not exist <DELIVERY_DIR>\%CUSTOMER% mkdir <DELIVERY_DIR>\%CUSTOMER%
6.6 .gitignore 只忽略 *.zip
交付仓库的 .gitignore 若误把 dist/、H5api/、scripts/ 也忽略了,git add -A 不提交它们,最后拉下来打包是空的。只写 *.zip。
6.7 空 commit 仍 push 报错
没有变更时 git commit 为空,若仍执行 git push 会报 src refspec <branch> does not match any。脚本里已做判断:commit 失败(空)就跳过 push。同时确认交付仓库默认分支名(常见 master / main),推送分支名须与实际一致。
6.8 交付仓库地址命名空间写错
不同仓库的 Git 命名空间可能不同(比如后端在 <GROUP>/h5-api.git,而交付仓库在另一个 <GROUP>/delivery-repo.git)。地址报 not found 时,先核对命名空间。
6.9 失败即停,别连锁报错
前面某步失败但没 exit,后续命令继续执行会爆出一堆"系统找不到指定的路径"。每个关键操作后加 if errorlevel 1 exit /b 1,失败即停,定位更快。
6.10 仓库地址里的 @ 转义
批处理环境里 Git URL 的 @ 需写成 %%40(仅当用户名含 @ 时)。用令牌方式(<user>:<token>)通常不含 @,可忽略。
7. 第五章:多客户 / 多平台(异构)怎么打
不同客户服务器操作系统 + CPU 架构不同,发布方式分两类:
| 模式 | 场景 | 发布方式 |
|---|---|---|
| A. 自研环境 | dev/test/发版(单机 Windows) | 环境变量注入,dotnet xxx.dll 直接跑(framework-dependent) |
| B. 交付客户 | 各客户自有服务器 | -r <RID> --self-contained true,打 zip 交付,客户自行部署 |
目标平台 RID 对照(重点)
| 客户服务器 | RID | 说明 |
|---|---|---|
| Windows 64位 | win-x64 | 普通 Windows Server |
| Windows 32位 | win-x86 | 老旧系统 |
| Linux x64(海光/兆芯) | linux-x64 | 麒麟/统信 x86 版 |
| Linux ARM64(飞腾/鲲鹏) | linux-arm64 | 麒麟/统信 ARM 版(最常见) |
| 龙芯 | linux-loongarch64 | 需对应 .NET 支持版本 |
⚠️ 部署前必须问客户:操作系统 + CPU 架构。同是"麒麟",飞腾芯要
arm64,海光芯要x64,选错跑不起来。--self-contained true自带 .NET 运行时,客户机免装 .NET。
客户部署方式:
- Windows:双击 exe;
- Linux:
chmod +x <YourApiProject> && ./<YourApiProject>。
8. 第六章:交付包发版前核验清单
出包后、发给客户前,必查 5 条:
- 顶层结构 =
dist/+H5api/(外加nginx部署包/与部署文档); H5api/api/含<YourApiProject>(Linux 下无.exe后缀)+appsettings.json;H5api/api/内有.so文件(libcoreclr.so/libhostfxr.so/libclrjit.so等)→ 确认 self-contained 完整,客户机免装 .NET;H5api/根有start/stop/restart-api.sh或.bat,内容无乱码;dist/index.html存在。
想自动化核验?用 Python 一行
zipfile.ZipFile(path).namelist()检查上述路径是否都在即可。
9. 结语
配好一次之后,换客户 / 换平台只要改 5 个参数点构建即可,约 20~30 分钟自动出包,包结构永远一致、客户机解压即部署。
这套流水线的精髓就三句话:
- 配置驱动差异化:前端靠
.env+build:<客户>脚本,后端靠环境变量注入,绝不在脚本里写死客户配置; - 产物可追溯:构建产物推 Git、打 zip、Jenkins 归档,浏览器直接下载,无需登服务器;
- 失败即停 + 清单核验:把不确定性挡在发版之前。
照着这篇从环境、任务配置、完整脚本到踩坑清单一步步走,你也能拥有一套"填参数就出包"的交付流水线。
附:一键自查表
| 检查项 | 是否就绪 |
|---|---|
| Jenkins 能启动、能访问 | ☐ |
交付仓库 scripts/ 含 6 个启停脚本 | ☐ |
.gitignore 只忽略 *.zip | ☐ |
前端已提交 .env.<客户> + build:<客户> | ☐ |
| 参数化任务 5 个参数已建 | ☐ |
| 构建后操作已配 Archive the artifacts | ☐ |
| 已知客户操作系统 + CPU 架构 | ☐ |
出问题先对照第四章踩坑表自查,80% 的报错都在里面。