[EQD-6838] bundle 重建优化:挂 Build 触发点 + CI 强制全量 + 增量检查

- csproj: GenerateBundlesBeforePublish 改为 GenerateBundlesBeforeBuild,
  挂 BeforeTargets=Build 让 IDE F5/Rebuild 也能自动重建,开发者无需记命令
- rebuild-bundles.ps1/py: 加 CI 环境检测(CI/JENKINS_HOME/BUILD_NUMBER),
  CI 下强制全量重建避免 git checkout 后 mtime 不可靠;本地开发走增量检查
- rebuild-bundles.ps1: BOM 在重建模式下只警告不 fail(重建会自动剥离),
  避免 form-layout.css 的 BOM 阻塞 CI 部署
- csproj: 新增独立 VerifyBundles target 供手动校验,不影响 Build/Publish
- csproj: 新增 SkipBundleGeneration 属性开关,特殊场景可跳过自动重建
- deploy/jenkins-build.sh: 添加 Jenkins 构建脚本参考副本(不参与 CI 执行),
  更新注释说明 target 改名后的 CI 行为
This commit is contained in:
hjhan
2026-07-29 18:54:38 +08:00
parent 76f5caabd9
commit 54b8ae5cf5
4 changed files with 220 additions and 7 deletions
+21 -4
View File
@@ -232,10 +232,12 @@
</ItemGroup>
<!-- 发布前按 bundleconfig.json 确定性重建前端 bundle 产物(jquery.js/bundle.js/vue.js/bundle.css/bundleV2.css/bundleV2.js)。
脚本委托 python3;若 CI/本机无 python3 则优雅跳过,回退为仓库内已提交的产物
这样改动前端源文件后发布会自动重新打 bundle,无需手工记得到处重建(EQD-6838)。 -->
<Target Name="GenerateBundlesBeforePublish" BeforeTargets="Publish">
<!-- Build 前按 bundleconfig.json 增量重建前端 bundle 产物(jquery.js/bundle.js/vue.js/bundle.css/bundleV2.css/bundleV2.js)。
挂在 Build 而非 Publish 上:IDE 里 F5 启动 / Rebuild 也能自动重建,开发者无需记命令行
脚本内部做时间戳增量检查:后端-only 改动时所有源文件都不比 bundle 新,秒级 UP-TO-DATE 跳过,不影响日常编译速度。
只有前端源文件变更时才重建对应 bundle。
Agent 远程改源文件 / 跨平台开发者无 PowerShell 等场景下,CI 仍能产出正确 bundle(EQD-6838)。 -->
<Target Name="GenerateBundlesBeforeBuild" BeforeTargets="Build" Condition="'$(SkipBundleGeneration)' != 'true'">
<PropertyGroup>
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
@@ -246,6 +248,21 @@
Command="bash &quot;$(_RebuildScriptSh)&quot;" />
</Target>
<!-- 手动校验 targetdotnet build -t:VerifyBundles
不重建、不改文件,仅比对源文件与已提交 bundle 产物是否一致。
用于开发本地检查"我改了源文件但忘了跑 rebuild 并提交"的情况。
CI 部署不需要调用此 target(部署走 GenerateBundlesBeforeBuild 自动重建)。 -->
<Target Name="VerifyBundles">
<PropertyGroup>
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
</PropertyGroup>
<Exec Condition="'$(OS)' == 'Windows_NT'"
Command="powershell -ExecutionPolicy Bypass -File &quot;$(_RebuildScript)&quot; -Verify" />
<Exec Condition="'$(OS)' != 'Windows_NT'"
Command="bash &quot;$(_RebuildScriptSh)&quot; --verify" />
</Target>
<Target Name="CopyFilesAfterPublish" AfterTargets="Publish">
<Copy SourceFiles="@(_AppFiles)" DestinationFolder="$(PublishDir)\%(RecursiveDir)" />
<Copy SourceFiles="@(ViewFiles)" DestinationFolder="$(PublishDir)\wwwroot\Statics\Views\%(RecursiveDir)" />
+34 -1
View File
@@ -35,11 +35,38 @@ $bundles = $configJson | ConvertFrom-Json
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
$mismatches = @()
# CI 环境检测:CI 下 git checkout 后所有文件 mtime 几乎相同,增量检查不可靠
# 因此 CI 环境强制全量重建,本地开发才走增量
$isCI = -not [string]::IsNullOrEmpty($env:CI) `
-or -not [string]::IsNullOrEmpty($env:JENKINS_HOME) `
-or -not [string]::IsNullOrEmpty($env:BUILD_NUMBER) `
-or -not [string]::IsNullOrEmpty($env:GITHUB_ACTIONS)
foreach ($bundle in $bundles) {
$outputRel = $bundle.outputFileName
$outputPath = Join-Path $projectDir $outputRel
$isMinified = $bundle.minify.enabled
# 增量检查:非 Verify、非 CI、非 minified,且 bundle 产物已存在时,
# 若所有源文件都不比 bundle 产物新,则跳过此 bundle
# 目的:让 target 挂到 Build 之前时,后端-only 改动不会触发重建,IDE F5 不变慢
# CI 环境强制全量:git checkout 后 mtime 不可靠,必须重建保证部署产物正确
if (-not $Verify -and -not $isCI -and -not $isMinified -and (Test-Path $outputPath)) {
$outputMtime = (Get-Item $outputPath).LastWriteTime
$stale = $false
foreach ($inputRel in $bundle.inputFiles) {
$inputPath = Join-Path $projectDir $inputRel
if ((Test-Path $inputPath) -and (Get-Item $inputPath).LastWriteTime -gt $outputMtime) {
$stale = $true
break
}
}
if (-not $stale) {
Write-Host ("[UP-TO-DATE] {0}" -f $outputRel) -ForegroundColor DarkGray
continue
}
}
Write-Host ("[{0}] {1}" -f $(if ($isMinified) { 'SKIP-MIN' } else { 'BUILD' }), $outputRel)
# Check input files exist + no BOM
@@ -73,10 +100,16 @@ foreach ($bundle in $bundles) {
continue
}
if ($bomInputs.Count -gt 0) {
Write-Host " [ERROR] Input files have BOM:" -ForegroundColor Red
# Verify 模式:BOM 是源文件问题,报错阻止提交
# 重建模式:BOM 会在读取时自动剥离(ReadAllText 处理),只警告不 fail,避免阻塞 CI 部署
$level = $(if ($Verify) { 'ERROR' } else { 'WARN' })
$color = $(if ($Verify) { 'Red' } else { 'Yellow' })
Write-Host " [$level] Input files have BOM (will be stripped on rebuild):" -ForegroundColor $color
foreach ($b in $bomInputs) { Write-Host " - $b" }
if ($Verify) {
$mismatches += "$outputRel : input BOM ($($bomInputs -join ', '))"
}
}
# Pure concatenation with LF separator between files (matches committed bundle artifacts)
$content = [string]::Join("`n", $parts)
+59 -1
View File
@@ -97,9 +97,67 @@ def build_all():
return results
def is_stale(out, input_files):
"""增量检查:任一源文件比 bundle 产物新(或产物不存在)则返回 True。"""
dest = os.path.join(ROOT, out)
if not os.path.exists(dest):
return True
out_mtime = os.path.getmtime(dest)
for f in input_files:
p = os.path.join(ROOT, f)
if os.path.exists(p) and os.path.getmtime(p) > out_mtime:
return True
return False
def is_ci_environment():
"""检测是否在 CI 环境运行。
CI 下 git checkout 后所有文件 mtime 几乎相同,增量检查不可靠,
因此 CI 环境强制全量重建。
"""
ci_vars = ('CI', 'JENKINS_HOME', 'BUILD_NUMBER', 'GITHUB_ACTIONS',
'GITLAB_CI', 'TF_BUILD')
return any(os.environ.get(v) for v in ci_vars)
def main():
verify = '--verify' in sys.argv[1:]
results = build_all()
force_rebuild = is_ci_environment() # CI 环境强制全量重建
# 读取配置用于增量检查
cfg_path = os.path.join(ROOT, 'bundleconfig.json')
raw = open(cfg_path, 'rb').read()
raw = re.sub(rb'//[^\n]*', b'', raw)
cfg = json.loads(raw.decode('utf-8-sig'))
# 增量过滤:非 verify、非 CI 时,跳过源文件未变更的 bundle
# CI 环境(force_rebuild=True)强制全量重建,因为 git checkout 后 mtime 不可靠
stale_bundles = []
for b in cfg:
out = b['outputFileName']
if verify or force_rebuild or is_stale(out, b['inputFiles']):
stale_bundles.append(b)
else:
print(f" [UP-TO-DATE] {out}")
# 只重建过期的 bundle
results = []
for b in stale_bundles:
out = b['outputFileName']
iscss = out.endswith('.css')
parts = []
missing = []
for f in b['inputFiles']:
p = os.path.join(ROOT, f)
if not os.path.exists(p):
missing.append(f)
continue
content = normalize(open(p, 'rb').read())
if iscss:
content = rewrite_css_urls(content, f, out)
parts.append(content)
joined = b'\n'.join(parts)
results.append((out, joined, missing))
ok = True
for out, joined, missing in results:
dest = os.path.join(ROOT, out)
+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
# =============================================================================
# Jenkins CI 构建脚本(参考副本)
# =============================================================================
# 此文件为 Jenkins 节点上实际执行的构建脚本的参考副本。
# 实际执行以 Jenkins 节点上的脚本为准;本副本用于:
# 1. 让团队了解 CI 构建流程(大小写修正、publish、部署、重启)
# 2. 修改 CI 流程时有参考依据
# 3. 排查部署问题时可对照实际行为
#
# 注意:本文件不参与 CI 执行。修改此文件不会影响 Jenkins 构建。
# 如需修改 CI 流程,请联系运维同步修改 Jenkins 节点上的脚本。
# =============================================================================
set -euo pipefail
export DOTNET_ROOT=/usr/local/dotnet
export PATH=$PATH:$DOTNET_ROOT
export JENKINS_NODE_COOKIE=dontKillMe
export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
WS="$WORKSPACE"
# ============================================================
# 1. 文件名大小写修正(Linux 区分大小写)
# ============================================================
# Windows 不区分大小写,但 Linux 区分。
# 以下文件在仓库里的命名与 Linux 期望不一致,需要重命名。
echo "Step 1: Fix filename casing..."
safe_rename() {
local src="$1" dst="$2"
[[ "$src" == "$dst" ]] && return
if [[ -d "$src" ]]; then
mkdir -p "$dst"
rsync -a "$src"/ "$dst"/
rm -rf "$src"
elif [[ -f "$src" ]]; then
mkdir -p "$(dirname "$dst")"
mv -f "$src" "$dst"
fi
}
safe_rename "$WS/Framework/YLErp.Resources/Dictionary" "$WS/Framework/YLErp.Resources/dictionary"
safe_rename "$WS/YLErpDAL/Resources" "$WS/YLErpDAL/resources"
safe_rename "$WS/YLErpDAL/resources/clientEditConfig.js" "$WS/YLErpDAL/resources/clienteditconfig.js"
# ============================================================
# 2. 清理 & 发布
# ============================================================
# dotnet publish 会触发 csproj 中的 GenerateBundlesBeforeBuild target(挂在 Build 之前),
# 按 bundleconfig.json 增量重建前端 bundle 产物。
# CI 环境(JENKINS_HOME 存在)下脚本会强制全量重建,不依赖 git checkout 后的 mtime。
echo "Step 2: Clean & publish..."
rm -rf "$WS/otc" "$WS/RealTimeCalcPositionService"
publish() {
local name="$1" path="$2" output="$3"
echo " Publishing $name..."
cd "$path"
dotnet publish "${name}.csproj" -c Release -o "$output" --nologo
}
publish "YLErpWeb" "$WS/YLErpWeb" "$WS/otc"
publish "RealTimeCalcPositionService" "$WS/YLWinSer/RealTimeCalcPositionService" "$WS/RealTimeCalcPositionService"
publish "YLErp.Plugins.GuoLian" "$WS/Plugins/YLErp.Plugins.GuoLian" "$WS/otc/App_Plugin"
# ============================================================
# 3. 移除敏感文件
# ============================================================
echo "Step 3: Remove sensitive configs..."
rm -f "$WS/RealTimeCalcPositionService/appsettings"* "$WS/otc/appsettings"*
rm -f "$WS/RealTimeCalcPositionService/st"* "$WS/otc/st"*
# ============================================================
# 4. 同步 & 保留特定插件文件
# ============================================================
echo "Step 4: Sync to deploy directory..."
rsync -a "$WS/otc/" "/home/glms/YLErpWeb/"
rsync -a "$WS/RealTimeCalcPositionService/" "/home/glms/RealTimeCalcPositionService/"
# App_Docs 移到上级目录后删掉原位置
rsync -a --delete "$WS/otc/App_Docs/" "$WS/App_Docs/"
rm -rf "$WS/otc/App_Docs"
# App_Plugin 只保留 GuoLian 相关
find "$WS/otc/App_Plugin" -mindepth 1 -maxdepth 1 \
! -name "YLErp.Plugins.GuoLian.deps.json" \
! -name "YLErp.Plugins.GuoLian.dll" \
! -name "YLErp.Plugins.GuoLian.pdb" \
-exec rm -rf {} +
# ============================================================
# 5. 重启服务(延迟脱离 Jenkins 进程树)
# ============================================================
echo "Step 5: Schedule restart..."
echo "cd /home/glms/RealTimeCalcPositionService && sh server-ctl.sh restart" | at now + 1 minute
echo "cd /home/glms/YLErpWeb && sh server-ctl.sh restart" | at now + 2 minute
echo "Done."