Skip to content

[Auto] Publish | release -> main | Release #9

[Auto] Publish | release -> main | Release

[Auto] Publish | release -> main | Release #9

name: Auto Sync and Publish
run-name: "[Auto] Publish | ${{ github.event.pull_request.head.ref }} -> main | ${{ github.event.pull_request.title }}"
on:
pull_request:
branches:
- main
types:
- closed
concurrency:
group: release-main
cancel-in-progress: false # 同一时间只允许一个构建,确保版本号不发生竞争
permissions:
contents: write
statuses: write
jobs:
# ─────────────────────────────────────────────────────────────
# Job 1: 准备阶段 - 记录 SHA、确定分支、计算新版本号
# ─────────────────────────────────────────────────────────────
prepare:
name: Prepare - Calculate Version & Record SHAs
runs-on: windows-latest
if: github.event.pull_request.merged == true
outputs:
source_branch: ${{ steps.branch_info.outputs.source_branch }}
increment_type: ${{ steps.branch_info.outputs.increment_type }}
current_version: ${{ steps.version.outputs.current_version }}
new_version: ${{ steps.version.outputs.new_version }}
new_version_text: ${{ steps.version.outputs.new_version_text }}
current_version_text: ${{ steps.version.outputs.current_version_text }}
main_sha: ${{ steps.shas.outputs.main_sha }}
dev_sha: ${{ steps.shas.outputs.dev_sha }}
bugfix_sha: ${{ steps.shas.outputs.bugfix_sha }}
release_sha: ${{ steps.shas.outputs.release_sha }}
pr_title: ${{ steps.pr_info.outputs.pr_title }}
pr_body: ${{ steps.pr_info.outputs.pr_body }}
steps:
- name: Checkout with full history
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Record original branch SHAs
id: shas
shell: powershell
run: |
$mainSha = git rev-parse HEAD
Write-Output "main_sha=$mainSha" >> $env:GITHUB_OUTPUT
Write-Host "main SHA: $mainSha"
$devSha = git rev-parse origin/dev
Write-Output "dev_sha=$devSha" >> $env:GITHUB_OUTPUT
Write-Host "dev SHA: $devSha"
$bugfixSha = git rev-parse origin/bugfix
Write-Output "bugfix_sha=$bugfixSha" >> $env:GITHUB_OUTPUT
Write-Host "bugfix SHA: $bugfixSha"
$releaseSha = git rev-parse origin/release
Write-Output "release_sha=$releaseSha" >> $env:GITHUB_OUTPUT
Write-Host "release SHA: $releaseSha"
- name: Determine source branch and version increment
id: branch_info
shell: powershell
env:
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
$sourceBranch = $env:PR_HEAD_REF
$incrementType = "patch"
if ($sourceBranch -eq "release") {
$incrementType = "feature"
Write-Host "Source branch: release - incrementing minor version (feature update)"
} elseif ($sourceBranch -eq "dev") {
$incrementType = "minor"
Write-Host "Source branch: dev - incrementing build version (daily update)"
} elseif ($sourceBranch -eq "bugfix") {
$incrementType = "patch"
Write-Host "Source branch: bugfix - incrementing patch version"
} else {
Write-Host "Unknown source branch: $sourceBranch - defaulting to patch"
}
Write-Output "source_branch=$sourceBranch" >> $env:GITHUB_OUTPUT
Write-Output "increment_type=$incrementType" >> $env:GITHUB_OUTPUT
- name: Calculate new version
id: version
shell: powershell
run: |
$csprojPath = "src/VirtualPaper/VirtualPaper.csproj"
$content = Get-Content $csprojPath -Raw
$versionMatch = [regex]::Match($content, '<AssemblyVersion>(\d+)\.(\d+)\.(\d+)\.(\d+)</AssemblyVersion>')
if (-not $versionMatch.Success) {
Write-Error "Could not parse version from csproj file"
exit 1
}
$major = [int]$versionMatch.Groups[1].Value
$minor = [int]$versionMatch.Groups[2].Value
$build = [int]$versionMatch.Groups[3].Value
$revision = [int]$versionMatch.Groups[4].Value
$currentVersion = "$major.$minor.$build.$revision"
$incrementType = "${{ steps.branch_info.outputs.increment_type }}"
if ($incrementType -eq "feature") {
$minor = $minor + 1 # 第2位 +1
$build = 0 # 第3位 清零
$revision = 0 # 第4位 清零
} elseif ($incrementType -eq "minor") {
$build = $build + 1 # 第3位 +1
$revision = 0 # 第4位 清零
} else {
$revision = $revision + 1 # 第4位 +1
}
$newVersion = "$major.$minor.$build.$revision"
$newVersionText = "$major$minor$build$revision"
Write-Host "Version: $currentVersion -> $newVersion"
Write-Output "current_version=$currentVersion" >> $env:GITHUB_OUTPUT
Write-Output "new_version=$newVersion" >> $env:GITHUB_OUTPUT
Write-Output "new_version_text=$newVersionText" >> $env:GITHUB_OUTPUT
- name: Capture PR title and body
id: pr_info
shell: powershell
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# PowerShell 5.1 的 >> 和 Out-File -Encoding utf8 均会写入 UTF-16 LE 或带 BOM 的 UTF-8
# $GITHUB_OUTPUT 必须是无 BOM 的 UTF-8,使用 .NET API 显式指定
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
# PR title(单行)
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "pr_title=$env:PR_TITLE`n", $utf8NoBom)
Write-Host "PR title: $env:PR_TITLE"
# PR body(多行,heredoc delimiter 格式,完整保留换行和特殊字符)
$delimiter = [System.Guid]::NewGuid().ToString("N")
$body = $env:PR_BODY -replace "`r`n", "`n" # 统一为 LF,避免 CRLF 写入 GITHUB_OUTPUT
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "pr_body<<$delimiter`n", $utf8NoBom)
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "$body`n", $utf8NoBom)
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "$delimiter`n", $utf8NoBom)
Write-Host "PR body captured ($($body.Length) chars)"
# ─────────────────────────────────────────────────────────────
# Job 2: 编译 .NET Release
# ─────────────────────────────────────────────────────────────
build:
name: Build .NET Release
runs-on: windows-latest
needs: prepare
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('src/**/*.csproj', 'src/**/*.props') }}
restore-keys: nuget-${{ runner.os }}-
- name: Patch AssemblyVersion in csproj (local only, not committed)
shell: powershell
run: |
$csprojPath = "src/VirtualPaper/VirtualPaper.csproj"
$content = Get-Content $csprojPath -Raw
$newVersion = "${{ needs.prepare.outputs.new_version }}"
$newContent = $content -replace '<AssemblyVersion>\d+\.\d+\.\d+\.\d+</AssemblyVersion>', "<AssemblyVersion>$newVersion</AssemblyVersion>"
Set-Content $csprojPath -Value $newContent -NoNewline
Write-Host "Patched AssemblyVersion to $newVersion (build-time only)"
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Rebuild solution
run: msbuild src/VirtualPaper.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU" /restore /m
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: build-release-output
path: src/
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Job 3: 制作安装包(依赖 build 完成)
# ─────────────────────────────────────────────────────────────
package:
name: Create Installer
runs-on: windows-latest
needs: [prepare, build]
outputs:
installer_name: ${{ steps.installer_info.outputs.installer_name }}
installer_path: ${{ steps.installer_info.outputs.installer_path }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download build output
uses: actions/download-artifact@v4
with:
name: build-release-output
path: src/
- name: Install Inno Setup 6.7.1
shell: powershell
run: choco install innosetup --version=6.7.1 --no-progress -y
- name: Patch version in InnoSetup script (local only, not committed)
shell: powershell
run: |
$setupPath = "InnoSetup/setup.iss"
$content = Get-Content $setupPath -Raw
$newVersion = "${{ needs.prepare.outputs.new_version }}"
$newVersionText = "${{ needs.prepare.outputs.new_version_text }}"
$newContent = $content -replace '#define MyAppVersion "[\d\.]+"', "#define MyAppVersion `"$newVersion`""
$newContent = $newContent -replace '#define MyAppVersionText "[\d]+"', "#define MyAppVersionText `"$newVersionText`""
Set-Content $setupPath -Value $newContent -NoNewline
Write-Host "Patched InnoSetup version to $newVersion (build-time only)"
- name: Download InnoDependencyInstaller
shell: powershell
run: |
$url = "https://raw.githubusercontent.com/DomGries/InnoDependencyInstaller/master/CodeDependencies.iss"
$dest = "InnoSetup\CodeDependencies.iss"
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " CodeDependencies.iss downloaded"
- name: Compile installer
id: build_installer
shell: powershell
run: |
$iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) {
Write-Error "ISCC.exe not found at: $iscc"
exit 1
}
& $iscc "InnoSetup\setup.iss" /O+
if ($LASTEXITCODE -ne 0) {
Write-Error "Inno Setup compilation failed (exit code $LASTEXITCODE)"
exit 1
}
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " Installer compiled successfully"
- name: Record installer info
id: installer_info
shell: powershell
run: |
$outputDir = "InnoSetup\Output"
$installerFiles = Get-ChildItem -Path $outputDir -Filter "*.exe" -ErrorAction SilentlyContinue
if ($installerFiles.Count -eq 0) {
Write-Error "No installer found in output directory!"
exit 1
}
$installerPath = $installerFiles[0].FullName
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " Installer: $installerPath"
Write-Output "installer_path=$installerPath" >> $env:GITHUB_OUTPUT
Write-Output "installer_name=$($installerFiles[0].Name)" >> $env:GITHUB_OUTPUT
- name: Generate SHA256 for installer
shell: powershell
run: |
$hash = (Get-FileHash -Path "${{ steps.installer_info.outputs.installer_path }}" -Algorithm SHA256).Hash.ToLower()
Set-Content -Path "${{ github.workspace }}\InnoSetup\Output\SHA256.txt" -Value $hash
- name: Upload installer and SHA256 as artifact
uses: actions/upload-artifact@v4
with:
name: VirtualPaper-Installer-v${{ needs.prepare.outputs.new_version }}
path: |
${{ steps.installer_info.outputs.installer_path }}
InnoSetup/Output/SHA256.txt
retention-days: 30
# ─────────────────────────────────────────────────────────────
# Job 4: 安装包冒烟测试(静默安装 + 启动验证)
# 阶段 1 - 等待 VirtualPaper.exe 自动拉起 UI(最多 10s)
# 阶段 2 - 兜底:若未自动拉起则手动启动 UI
# 阶段 3 - 再次尝试启动 UI,断言全局只有 1 个 UI 进程(单例守卫)
# 阶段 4 - 验证 UI 无法在主进程未启动的情况下独立运行(应弹出错误 MessageBox 并阻塞)
# ─────────────────────────────────────────────────────────────
smoke_test:
name: Smoke Test Installer
runs-on: windows-latest
needs: [prepare, package]
steps:
- name: Setup .NET (app runtime dependency)
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Download installer artifact
uses: actions/download-artifact@v4
with:
name: VirtualPaper-Installer-v${{ needs.prepare.outputs.new_version }}
path: dist/
- name: Silent install
id: install
shell: powershell
run: |
$installer = Get-ChildItem "dist\" -Filter "*.exe" | Select-Object -First 1
if (-not $installer) {
Write-Error "No installer found in dist\"
exit 1
}
$installDir = "C:\VirtualPaperSmokeTest"
Write-Host "Installer : $($installer.FullName)"
Write-Host "Target dir: $installDir"
$proc = Start-Process -FilePath $installer.FullName `
-ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /DIR=`"$installDir`"" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Installer failed with exit code $($proc.ExitCode)"
exit 1
}
Write-Output "install_dir=$installDir" >> $env:GITHUB_OUTPUT
Write-Host "[OK] Installation completed"
- name: Verify key files exist
shell: powershell
run: |
$base = "${{ steps.install.outputs.install_dir }}"
$required = @(
"$base\VirtualPaper.exe",
"$base\Plugins\UI\VirtualPaper.UI.exe"
)
$failed = $false
foreach ($f in $required) {
if (Test-Path $f) {
Write-Host "[OK] $f"
} else {
Write-Host "[MISSING] $f" -ForegroundColor Red
$failed = $true
}
}
if ($failed) {
Write-Error "Required files missing after installation"
exit 1
}
- name: Launch and verify UI process
shell: powershell
run: |
$installDir = "${{ steps.install.outputs.install_dir }}"
$mainExe = "$installDir\VirtualPaper.exe"
$uiExe = "$installDir\Plugins\UI\VirtualPaper.UI.exe"
# ── 阶段 1:启动主程序,等待其自动拉起 UI(最多 10s)──────────────
Write-Host "Launching: $mainExe"
$main = Start-Process -FilePath $mainExe -PassThru -ErrorAction Stop
$uiProc = $null
$autoWait = 10
for ($i = 1; $i -le $autoWait; $i++) {
Start-Sleep -Seconds 1
$main.Refresh()
if ($main.HasExited) {
Write-Error "VirtualPaper.exe exited unexpectedly after ${i}s (exit code: $($main.ExitCode))"
exit 1
}
$uiProc = Get-Process -Name "VirtualPaper.UI" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($uiProc) {
Write-Host "[OK] VirtualPaper.UI auto-started after ${i}s (PID: $($uiProc.Id))"
break
}
Write-Host " [wait] ${i}s - waiting for VirtualPaper.UI to be auto-spawned..."
}
# ── 阶段 2:兜底 - 若未自动拉起则手动启动 UI ──────────────────────
if (-not $uiProc) {
Write-Host "[FALLBACK] UI not auto-spawned after ${autoWait}s, launching manually: $uiExe"
Start-Process -FilePath $uiExe -ErrorAction Stop
Start-Sleep -Seconds 5
$uiProc = Get-Process -Name "VirtualPaper.UI" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $uiProc) {
Write-Error "VirtualPaper.UI.exe failed to start even when launched manually"
taskkill /F /T /PID $main.Id 2>$null
exit 1
}
Write-Host "[OK] VirtualPaper.UI started via fallback (PID: $($uiProc.Id))"
}
# ── 阶段 3:单例守卫验证 - 再次启动 UI,期望仍只有 1 个进程 ────────
Write-Host "Verifying singleton: launching VirtualPaper.UI again..."
Start-Process -FilePath $uiExe -ErrorAction SilentlyContinue
Start-Sleep -Seconds 3
$uiProcs = @(Get-Process -Name "VirtualPaper.UI" -ErrorAction SilentlyContinue)
if ($uiProcs.Count -ne 1) {
Write-Error "Singleton check FAILED: expected 1 VirtualPaper.UI process, found $($uiProcs.Count)"
taskkill /F /T /PID $main.Id 2>$null
Get-Process -Name "VirtualPaper.UI" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
exit 1
}
Write-Host "[OK] Singleton check passed: only 1 VirtualPaper.UI process (PID: $($uiProcs[0].Id))"
# ── 清理 ───────────────────────────────────────────────────────────
taskkill /F /T /PID $main.Id 2>$null
Get-Process -Name "VirtualPaper.UI" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Write-Host "[OK] Smoke test passed"
- name: Verify UI cannot start without main process
shell: powershell
run: |
$installDir = "${{ steps.install.outputs.install_dir }}"
$uiExe = "$installDir\Plugins\UI\VirtualPaper.UI.exe"
# 前置清理:确保主进程与 UI 进程均未运行(强杀残留)
@("VirtualPaper", "VirtualPaper.UI") | ForEach-Object {
$procs = Get-Process -Name $_ -ErrorAction SilentlyContinue
if ($procs) {
Write-Host "[CLEANUP] Killing residual process: $_"
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
}
}
Start-Sleep -Seconds 1
# ── 直接启动 UI(不启动主进程)──────────────────────────────────────
Write-Host "Launching VirtualPaper.UI.exe without main process..."
$uiProc = Start-Process -FilePath $uiExe -PassThru -ErrorAction Stop
# 等待足够时间让 MessageBox 弹出
Start-Sleep -Seconds 3
# ── 断言:进程应仍在运行(被 MessageBox 阻塞,未正常退出)────────────
$uiProc.Refresh()
if ($uiProc.HasExited) {
Write-Error "VirtualPaper.UI exited on its own - expected it to be blocked by a MessageBox"
exit 1
}
# ── 进一步断言:确认是 MessageBox(#32770)而非 UI 正常启动 ──────────
# 通过 UIAutomation 枚举该进程的顶层窗口并读取 Win32 ClassName
# MessageBox 的 ClassName 固定为 #32770,无需注入 C# 代码
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$desktop = [System.Windows.Automation.AutomationElement]::RootElement
$pidCond = New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::ProcessIdProperty,
$uiProc.Id)
$wins = $desktop.FindAll([System.Windows.Automation.TreeScope]::Children, $pidCond)
$classes = @($wins | ForEach-Object { $_.Current.ClassName })
Write-Host " Visible window classes: $($classes -join ', ')"
if (-not ($classes -contains '#32770')) {
Write-Error "Expected a MessageBox (#32770) but none found - UI may have started normally, guard logic broken"
Stop-Process -Id $uiProc.Id -Force -ErrorAction SilentlyContinue
exit 1
}
Write-Host "[OK] VirtualPaper.UI is blocked by a MessageBox (#32770) as expected (PID: $($uiProc.Id))"
# ── 清理:强杀被阻塞的 UI 进程 ──────────────────────────────────────
Stop-Process -Id $uiProc.Id -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
$uiProc.Refresh()
if (-not $uiProc.HasExited) {
taskkill /F /PID $uiProc.Id 2>$null
}
Write-Host "[OK] Standalone-UI guard test passed"
# ─────────────────────────────────────────────────────────────
# Job 5: 将版本号递增变更提交到 main(冒烟测试通过后才提交)
# ─────────────────────────────────────────────────────────────
bump:
name: Bump Version to main
runs-on: windows-latest
needs: [prepare, package, smoke_test]
steps:
- name: Checkout main with full history
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_PAT }}
fetch-depth: 0
- name: Update version in csproj
shell: powershell
run: |
$csprojPath = "src/VirtualPaper/VirtualPaper.csproj"
$content = Get-Content $csprojPath -Raw
$newVersion = "${{ needs.prepare.outputs.new_version }}"
$newContent = $content -replace '<AssemblyVersion>\d+\.\d+\.\d+\.\d+</AssemblyVersion>', "<AssemblyVersion>$newVersion</AssemblyVersion>"
Set-Content $csprojPath -Value $newContent -NoNewline
Write-Host "Updated csproj version to: $newVersion"
- name: Update version in InnoSetup script
shell: powershell
run: |
$setupPath = "InnoSetup/setup.iss"
$content = Get-Content $setupPath -Raw
$newVersion = "${{ needs.prepare.outputs.new_version }}"
$newVersionText = "${{ needs.prepare.outputs.new_version_text }}"
$newContent = $content -replace '#define MyAppVersion "[\d\.]+"', "#define MyAppVersion `"$newVersion`""
$newContent = $newContent -replace '#define MyAppVersionText "[\d]+"', "#define MyAppVersionText `"$newVersionText`""
Set-Content $setupPath -Value $newContent -NoNewline
Write-Host "Updated InnoSetup version to: $newVersion"
- name: Update version badge in README
shell: powershell
run: |
$readmePath = "README.md"
$content = Get-Content $readmePath -Raw
$newVersion = "${{ needs.prepare.outputs.new_version }}"
$newContent = $content -replace '(img\.shields\.io/badge/release-v)[\d\.]+(-blue)', "`${1}$newVersion`$2"
Set-Content $readmePath -Value $newContent -NoNewline
Write-Host "Updated README badge version to: $newVersion"
- name: Commit and push to main
shell: powershell
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add src/VirtualPaper/VirtualPaper.csproj InnoSetup/setup.iss README.md
git commit -m "[Github CI] chore: bump version to ${{ needs.prepare.outputs.new_version }} [skip ci]"
git push origin main
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " Version ${{ needs.prepare.outputs.new_version }} committed to main"
# ─────────────────────────────────────────────────────────────
# Job 6: 将版本号同步到 pre-publish branches
# ─────────────────────────────────────────────────────────────
sync:
name: Sync Version to Pre-publish Branches
runs-on: windows-latest
needs: [prepare, bump]
steps:
- name: Checkout with full history
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_PAT }}
fetch-depth: 0
- name: Sync version files to pre-publish branches
shell: powershell
run: |
$newVersion = "${{ needs.prepare.outputs.new_version }}"
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
# 确保拿到 bump job 刚推送的最新 main
$ErrorActionPreference = 'Continue'
git fetch origin 2>&1 | Out-Null
$ErrorActionPreference = 'Stop'
foreach ($branch in @("release", "dev", "bugfix")) {
Write-Host ""
Write-Host "--- Syncing version to $branch ---"
git checkout -B $branch origin/$branch
# 直接从 origin/main 取版本相关文件,无需 merge,不产生冲突
git checkout origin/main -- src/VirtualPaper/VirtualPaper.csproj InnoSetup/setup.iss README.md
Write-Host " [OK] version files pulled from origin/main"
git add src/VirtualPaper/VirtualPaper.csproj InnoSetup/setup.iss README.md
# 若无变更则跳过 commit(分支本来就在最新版本时)
$status = git status --porcelain
if ($status) {
git commit -m "[Github CI] chore: sync version $newVersion from main [skip ci]"
git push origin $branch
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " Version $newVersion synced to $branch"
} else {
Write-Host "[SKIP] $branch already at version $newVersion"
}
}
# ─────────────────────────────────────────────────────────────
# Job 7: 汇总输出(始终执行)
# ─────────────────────────────────────────────────────────────
summary:
name: Summary
runs-on: ubuntu-latest
needs: [prepare, build, package, smoke_test, bump, sync]
if: >-
!cancelled() &&
needs.prepare.result == 'success' &&
needs.build.result == 'success' &&
needs.package.result == 'success' &&
needs.smoke_test.result == 'success' &&
needs.bump.result == 'success' &&
needs.sync.result == 'success'
steps:
- name: Print summary
run: |
INCREMENT="${{ needs.prepare.outputs.increment_type }}"
if [ "$INCREMENT" = "feature" ]; then TYPE="Major Feature Update"
elif [ "$INCREMENT" = "minor" ]; then TYPE="Daily Update"
else TYPE="Bugfix Update"; fi
echo ""
echo -e "\033[0;32m================= Build Complete =================\033[0m"
echo "Project : VirtualPaper"
echo -e "\033[0;36mVersion : ${{ needs.prepare.outputs.current_version }} -> ${{ needs.prepare.outputs.new_version }}\033[0m"
echo "Source : ${{ needs.prepare.outputs.source_branch }} -> main"
echo "Type : $TYPE"
echo -e "\033[0;36mPackage : ${{ needs.package.outputs.installer_name }}\033[0m"
echo ""
echo -e "\033[0;32m[OK]\033[0m Version updated and committed to main"
echo -e "\033[0;32m[OK]\033[0m Version synced to pre-publish branches (release / dev / bugfix)"
echo -e "\033[0;32m[OK]\033[0m Installer uploaded as Artifact"
echo -e "\033[0;32m[OK]\033[0m Smoke test passed (install + launch + singleton)"
echo -e "\033[0;32m[OK]\033[0m Draft GitHub Release will be created: v${{ needs.prepare.outputs.new_version }}"
echo -e "\033[0;32m==========================================\033[0m"
# ─────────────────────────────────────────────────────────────
# Job 8: 创建草稿 GitHub Release(所有步骤均成功后执行)
# - 等待 summary 通过,即代表全流程无任何错误
# - 以 PR body 作为 Release 正文
# - Tag / Release 标题均为 "v版本号"
# - 将 Installer exe 作为 Release 附件上传
# - 创建为草稿(draft),需人工验证后手动点击发布
# ─────────────────────────────────────────────────────────────
release:
name: Create Draft GitHub Release
runs-on: ubuntu-latest
needs: [prepare, package, summary]
if: needs.summary.result == 'success'
steps:
- name: Download installer artifact
uses: actions/download-artifact@v4
with:
name: VirtualPaper-Installer-v${{ needs.prepare.outputs.new_version }}
path: dist/
- name: Create Draft Release & Tag
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.prepare.outputs.new_version }}
name: v${{ needs.prepare.outputs.new_version }}
body: ${{ needs.prepare.outputs.pr_body }}
files: |
dist/*.exe
dist/SHA256.txt
draft: true # 草稿状态,不对外公开,等待人工验证后发布
make_latest: false # 草稿阶段不标记为最新,发布时再生效
token: ${{ secrets.GITHUB_TOKEN }}
# ─────────────────────────────────────────────────────────────
# Job 9: 失败回滚(任意 job 失败时执行)
# ─────────────────────────────────────────────────────────────
rollback:
name: Rollback on Failure
runs-on: windows-latest
needs: [prepare, build, package, smoke_test, bump, sync]
if: always() && failure() && needs.prepare.result == 'success'
steps:
- name: Checkout with full history
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_PAT }}
fetch-depth: 0
- name: Rollback changes
shell: powershell
run: |
Write-Host ""
Write-Host "[WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " Pipeline failure detected, starting rollback..."
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
$ErrorActionPreference = 'Continue'
git fetch origin 2>&1 | Out-Null
$ErrorActionPreference = 'Stop'
$mainSha = "${{ needs.prepare.outputs.main_sha }}"
$devSha = "${{ needs.prepare.outputs.dev_sha }}"
$bugfixSha = "${{ needs.prepare.outputs.bugfix_sha }}"
$releaseSha = "${{ needs.prepare.outputs.release_sha }}"
# --- 回滚 main ---
if ($mainSha) {
$ErrorActionPreference = 'Continue'
$currentMain = git rev-parse origin/main 2>&1
$ErrorActionPreference = 'Stop'
if ($LASTEXITCODE -eq 0 -and $currentMain -ne $mainSha) {
Write-Host "[ROLLBACK]" -ForegroundColor Cyan -NoNewline; Write-Host " main: reverting version bump commit"
git checkout -B main origin/main
git revert HEAD --no-commit
git commit -m "[Github CI] rollback: revert version bump [skip ci]"
git push origin main
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " main rolled back"
} else {
Write-Host "[SKIP] main: no changes detected, rollback not needed"
}
}
# --- 回滚 pre-publish branches (release / dev / bugfix) ---
$branchShaMap = @{
"release" = $releaseSha
"dev" = $devSha
"bugfix" = $bugfixSha
}
foreach ($b in $branchShaMap.Keys) {
$targetSha = $branchShaMap[$b]
if (-not $targetSha) { continue }
$ErrorActionPreference = 'Continue'
$currentSha = git rev-parse origin/$b 2>&1
$ErrorActionPreference = 'Stop'
if ($LASTEXITCODE -eq 0 -and $currentSha -ne $targetSha) {
Write-Host "[ROLLBACK]" -ForegroundColor Cyan -NoNewline; Write-Host " ${b}: reverting version sync commit"
git checkout -B $b origin/$b
git revert HEAD --no-commit
git commit -m "[Github CI] rollback: revert version sync [skip ci]"
git push origin $b
Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " $b rolled back"
} else {
Write-Host "[SKIP] ${b}: no changes detected, rollback not needed"
}
}
Write-Host ""
Write-Host "================ Build Failed ================" -ForegroundColor Red
Write-Host "[ERROR]" -ForegroundColor Red -NoNewline; Write-Host " Please check the following:"
Write-Host " - .NET 8.0 build result"
Write-Host " - InnoSetup script correctness"
Write-Host " - Version format validity"
Write-Host " - GitHub Token permissions"
Write-Host "==========================================" -ForegroundColor Red