[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:
@@ -232,10 +232,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<!-- 发布前按 bundleconfig.json 确定性重建前端 bundle 产物(jquery.js/bundle.js/vue.js/bundle.css/bundleV2.css/bundleV2.js)。
|
<!-- Build 前按 bundleconfig.json 增量重建前端 bundle 产物(jquery.js/bundle.js/vue.js/bundle.css/bundleV2.css/bundleV2.js)。
|
||||||
脚本委托 python3;若 CI/本机无 python3 则优雅跳过,回退为仓库内已提交的产物。
|
挂在 Build 而非 Publish 上:IDE 里 F5 启动 / Rebuild 也能自动重建,开发者无需记命令行。
|
||||||
这样改动前端源文件后发布会自动重新打 bundle,无需手工记得到处重建(EQD-6838)。 -->
|
脚本内部做时间戳增量检查:后端-only 改动时所有源文件都不比 bundle 新,秒级 UP-TO-DATE 跳过,不影响日常编译速度。
|
||||||
<Target Name="GenerateBundlesBeforePublish" BeforeTargets="Publish">
|
只有前端源文件变更时才重建对应 bundle。
|
||||||
|
Agent 远程改源文件 / 跨平台开发者无 PowerShell 等场景下,CI 仍能产出正确 bundle(EQD-6838)。 -->
|
||||||
|
<Target Name="GenerateBundlesBeforeBuild" BeforeTargets="Build" Condition="'$(SkipBundleGeneration)' != 'true'">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
|
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
|
||||||
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
|
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
|
||||||
@@ -246,6 +248,21 @@
|
|||||||
Command="bash "$(_RebuildScriptSh)"" />
|
Command="bash "$(_RebuildScriptSh)"" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<!-- 手动校验 target:dotnet 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 "$(_RebuildScript)" -Verify" />
|
||||||
|
<Exec Condition="'$(OS)' != 'Windows_NT'"
|
||||||
|
Command="bash "$(_RebuildScriptSh)" --verify" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<Target Name="CopyFilesAfterPublish" AfterTargets="Publish">
|
<Target Name="CopyFilesAfterPublish" AfterTargets="Publish">
|
||||||
<Copy SourceFiles="@(_AppFiles)" DestinationFolder="$(PublishDir)\%(RecursiveDir)" />
|
<Copy SourceFiles="@(_AppFiles)" DestinationFolder="$(PublishDir)\%(RecursiveDir)" />
|
||||||
<Copy SourceFiles="@(ViewFiles)" DestinationFolder="$(PublishDir)\wwwroot\Statics\Views\%(RecursiveDir)" />
|
<Copy SourceFiles="@(ViewFiles)" DestinationFolder="$(PublishDir)\wwwroot\Statics\Views\%(RecursiveDir)" />
|
||||||
|
|||||||
@@ -35,11 +35,38 @@ $bundles = $configJson | ConvertFrom-Json
|
|||||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||||
$mismatches = @()
|
$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) {
|
foreach ($bundle in $bundles) {
|
||||||
$outputRel = $bundle.outputFileName
|
$outputRel = $bundle.outputFileName
|
||||||
$outputPath = Join-Path $projectDir $outputRel
|
$outputPath = Join-Path $projectDir $outputRel
|
||||||
$isMinified = $bundle.minify.enabled
|
$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)
|
Write-Host ("[{0}] {1}" -f $(if ($isMinified) { 'SKIP-MIN' } else { 'BUILD' }), $outputRel)
|
||||||
|
|
||||||
# Check input files exist + no BOM
|
# Check input files exist + no BOM
|
||||||
@@ -73,9 +100,15 @@ foreach ($bundle in $bundles) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ($bomInputs.Count -gt 0) {
|
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" }
|
foreach ($b in $bomInputs) { Write-Host " - $b" }
|
||||||
$mismatches += "$outputRel : input BOM ($($bomInputs -join ', '))"
|
if ($Verify) {
|
||||||
|
$mismatches += "$outputRel : input BOM ($($bomInputs -join ', '))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pure concatenation with LF separator between files (matches committed bundle artifacts)
|
# Pure concatenation with LF separator between files (matches committed bundle artifacts)
|
||||||
|
|||||||
@@ -97,9 +97,67 @@ def build_all():
|
|||||||
return results
|
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():
|
def main():
|
||||||
verify = '--verify' in sys.argv[1:]
|
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
|
ok = True
|
||||||
for out, joined, missing in results:
|
for out, joined, missing in results:
|
||||||
dest = os.path.join(ROOT, out)
|
dest = os.path.join(ROOT, out)
|
||||||
|
|||||||
@@ -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."
|
||||||
Reference in New Issue
Block a user