Bump version to 1.912 (arm64 + portable ZIP + multi-language) #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 260406Cl PDIndexer 由来の CSManager release.yml | |
| # 260625Cl vdproj→WiX 移行 (計画書 .project-guidance/CSManager_WiX移行計画.md §5)。旧 vdproj ビルド経路 (devenv / DisableOutOfProcBuild / | |
| # CI 専用 sln / vdproj regex / 非原子的 git tag→push) を全廃し、PDIndexer の 5-job 構成 (prepare/build-x64/build-arm64/ | |
| # smoke-arm64/release) へ再編。x64 MSI + arm64 MSI + portable ZIP (x64/arm64) を同一 run でビルド・smoke し、1 回の | |
| # gh release create --target $GITHUB_SHA で原子的に公開する。CSManager 固有の簡素化 (PDIndexer 計画 D-CS-1/2/6): | |
| # * Crystallography.Native / xraylib(libxrl) を UI から使わない → devenv/native ビルド不要、smoke は arch 確認のみ | |
| # * glfw3.dll は死荷物 → staging/portable から除外、arm64 publish には元々来ない (混入検出のみ) | |
| # 不変条件「x64 は arm64 の成否に連動しない」を維持: release job は build-x64 成功のみ必須。arm64 (build/smoke) が | |
| # 失敗したら x64 のみ公開して †脚注を残す。FileVersion は prepare で 1 度算出し x64/arm64 双方へ渡して完全一致させる | |
| # (same-version cross-grade 要件)。 | |
| name: Build And Release Installer And Portable ZIP | |
| on: | |
| push: | |
| branches: [master] | |
| paths: | |
| - "CSManager/Version.cs" | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: "既存リリースを削除してリビルドする" | |
| type: boolean | |
| default: false | |
| dry_run: | |
| description: "x64+arm64 をビルド・smoke するが release は作らない (資産は artifact のみ)" | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| # ---- 1. prepare: 版数解析・リリース要否判定・FileVersion 算出 ---- | |
| prepare: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| needed: ${{ steps.version.outputs.needed }} | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| product_version: ${{ steps.version.outputs.product_version }} | |
| assembly_version: ${{ steps.version.outputs.assembly_version }} | |
| history_line: ${{ steps.version.outputs.history_line }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 # assembly_version 用に GITHUB_SHA の commit 時刻を git log で引くため | |
| - name: Parse version and check if release needed | |
| id: version | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DRY_RUN: ${{ inputs.dry_run }} | |
| run: | | |
| $content = Get-Content "CSManager/Version.cs" -Raw | |
| if ($content -notmatch 'ver(\d+\.\d+)\(') { throw "Could not parse version from Version.cs History." } | |
| $ver = $Matches[1] | |
| $tag = "v.$ver" | |
| $dryRun = $env:DRY_RUN -eq 'true' | |
| gh release view $tag 2>&1 | Out-Null | |
| $releaseExists = ($LASTEXITCODE -eq 0) | |
| $global:LASTEXITCODE = 0 | |
| if ($releaseExists -and -not $dryRun) { | |
| if ("${{ inputs.force }}" -eq "true") { | |
| Write-Host "Force mode: deleting existing release $tag..." | |
| gh release delete $tag --yes | |
| git push origin :refs/tags/$tag 2>&1 | Out-Null | |
| $global:LASTEXITCODE = 0 | |
| } else { | |
| Write-Host "Release $tag already exists. Skipping." | |
| "needed=false" >> $env:GITHUB_OUTPUT | |
| return | |
| } | |
| } | |
| $historyLine = ([regex]::Match($content, 'ver[^\r\n"]+').Value).Trim() | |
| # AssemblyVersion / FileVersion: x64⇔arm64 の same-version cross-grade には両 MSI の FileVersion 完全一致が必要。 | |
| # release commit (GITHUB_SHA) の commit 時刻にアンカーして決定論化する (同一 run の x64/arm64 がこの 1 値を共有)。 | |
| $now = ([System.DateTimeOffset]::Parse((git log -1 --format=%cI $env:GITHUB_SHA))).UtcDateTime | |
| $assemblyVer = "$($now.Year).$($now.Month).$($now.Day).$($now.ToString('HHmm'))" | |
| Write-Host "Version: $ver (tag: $tag, assembly: $assemblyVer, dryRun: $dryRun)" | |
| "needed=true" >> $env:GITHUB_OUTPUT | |
| "version=$ver" >> $env:GITHUB_OUTPUT | |
| "tag=$tag" >> $env:GITHUB_OUTPUT | |
| "product_version=0.$ver" >> $env:GITHUB_OUTPUT | |
| "assembly_version=$assemblyVer" >> $env:GITHUB_OUTPUT | |
| "history_line=$historyLine" >> $env:GITHUB_OUTPUT | |
| # ---- 2. build-x64: x64 MSI (+旧名コピー) + portable ZIP ---- | |
| build-x64: | |
| needs: prepare | |
| if: needs.prepare.outputs.needed == 'true' | |
| runs-on: windows-2025-vs2026 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| submodules: recursive # shared libs are git submodules | |
| - name: Setup .NET 10 | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: "10.0.x" | |
| - name: Restore managed dependencies | |
| shell: pwsh | |
| run: dotnet restore CSManager\CSManager.csproj | |
| # CSManager は Crystallography.Native / xraylib を UI から使わないため devenv / native ビルドは無い (D-CS-1)。 | |
| - name: Stage MSI payload (framework-dependent publish) | |
| shell: pwsh | |
| run: | | |
| $s = "artifacts\staging-msi-x64" | |
| dotnet publish CSManager\CSManager.csproj -c Release -p:Platform=x64 --self-contained false -o $s --no-restore ` | |
| -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} | |
| if ($LASTEXITCODE -ne 0) { throw "x64 publish failed." } | |
| Get-ChildItem $s -Filter "glfw3*.dll" -ErrorAction SilentlyContinue | Remove-Item -Force # glfw3.dll は死荷物 (D-CS-2) | |
| Copy-Item CSManagerSetup.Wix\LICENSE.rtf, CSManagerSetup.Wix\REQUIREMENT.rtf $s | |
| Get-ChildItem $s -Filter *.pdb | Remove-Item | |
| if (Test-Path "$s\README-PORTABLE.txt") { throw "README-PORTABLE.txt leaked (portable marker must not be in MSI)" } | |
| if (Test-Path "$s\coreclr.dll") { throw "self-contained runtime leaked into framework-dependent staging" } | |
| if (Test-Path "$s\glfw3.dll") { throw "glfw3.dll leaked (must be excluded per D-CS-2)" } | |
| if (-not (Test-Path "$s\CSManager.exe")) { throw "CSManager.exe missing from staging" } | |
| if (-not (Test-Path "$s\AMCSD.cdb3")) { throw "AMCSD.cdb3 (default database) missing from staging" } | |
| $expectedCultures = @('ja','de','fr','es','pt','it','ru','zh-Hans','zh-Hant','ko') | |
| $missingSat = $expectedCultures | Where-Object { -not (Get-ChildItem "$s\$_" -Filter *.resources.dll -ErrorAction SilentlyContinue) } | |
| if ($missingSat) { throw "satellite assemblies missing for culture(s): $($missingSat -join ', ')" } | |
| Write-Host "MSI staging OK: $((Get-ChildItem $s -Recurse -File).Count) files" | |
| - name: Build installer with WiX | |
| id: build_installer | |
| timeout-minutes: 10 | |
| shell: pwsh | |
| run: | | |
| dotnet build CSManagerSetup.Wix\CSManagerSetup.wixproj -c Release -p:InstallerPlatform=x64 ` | |
| -p:ProductVersion=${{ needs.prepare.outputs.product_version }} ` | |
| -p:StagingDir="$PWD\artifacts\staging-msi-x64" | |
| if ($LASTEXITCODE -ne 0) { throw "WiX build failed (exit $LASTEXITCODE)." } | |
| # 主名称 CSManager-setup.msi + 旧名互換コピー CSManagerSetup.msi (同一バイト、{software}Setup.msi 自動更新固定参照を維持) | |
| $msi = Get-Item "CSManagerSetup.Wix\bin\Release\CSManager-setup.msi" -ErrorAction SilentlyContinue | |
| if (-not $msi) { throw "CSManager-setup.msi was not found." } | |
| Write-Host "MSI built: $($msi.FullName) ($($msi.Length) bytes)" | |
| "msi_path=$($msi.FullName)" >> $env:GITHUB_OUTPUT | |
| $legacy = Join-Path (Split-Path $msi.FullName) "CSManagerSetup.msi" | |
| Copy-Item $msi.FullName $legacy -Force | |
| "legacy_msi_path=$legacy" >> $env:GITHUB_OUTPUT | |
| - name: Publish portable ZIP | |
| id: portable | |
| shell: pwsh | |
| run: | | |
| $publishDir = Join-Path $env:RUNNER_TEMP "CSManager-portable\CSManager" | |
| $zipPath = Join-Path (Get-Location) "CSManager-v.${{ needs.prepare.outputs.version }}.zip" | |
| if (Test-Path $publishDir) { Remove-Item -LiteralPath $publishDir -Recurse -Force } | |
| if (Test-Path $zipPath) { Remove-Item -LiteralPath $zipPath -Force } | |
| dotnet publish CSManager\CSManager.csproj ` | |
| -c Release -r win-x64 --self-contained true -p:Platform=x64 ` | |
| -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` | |
| -p:PublishSingleFile=false -p:PublishTrimmed=false -p:DebugType=None -p:DebugSymbols=false ` | |
| -o $publishDir | |
| if ($LASTEXITCODE -ne 0) { throw "x64 self-contained publish failed." } | |
| Get-ChildItem $publishDir -Filter "glfw3*.dll" -ErrorAction SilentlyContinue | Remove-Item -Force # glfw3.dll 除外 (D-CS-2) | |
| Copy-Item "LICENSE.md" $publishDir -Force | |
| Copy-Item "README-PORTABLE.txt" $publishDir -Force # portable 判定マーカー (MSI には入れない) | |
| Compress-Archive -Path (Join-Path (Split-Path $publishDir -Parent) "CSManager") -DestinationPath $zipPath -Force | |
| Write-Host "Portable ZIP built: $zipPath" | |
| "zip_path=$zipPath" >> $env:GITHUB_OUTPUT | |
| - name: Collect x64 release assets | |
| shell: pwsh | |
| run: | | |
| $out = Join-Path $env:RUNNER_TEMP "x64-out" | |
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |
| Copy-Item "${{ steps.build_installer.outputs.msi_path }}" $out | |
| Copy-Item "${{ steps.build_installer.outputs.legacy_msi_path }}" $out | |
| Copy-Item "${{ steps.portable.outputs.zip_path }}" $out | |
| Get-ChildItem $out | ForEach-Object { Write-Host "x64 asset: $($_.Name) ($($_.Length) bytes)" } | |
| - name: Upload x64 release assets | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: x64-release-assets | |
| path: ${{ runner.temp }}/x64-out | |
| # ---- 3. build-arm64: reusable build。prepare の FileVersion を渡して x64 と一致させる ---- | |
| build-arm64: | |
| needs: prepare | |
| if: needs.prepare.outputs.needed == 'true' | |
| uses: ./.github/workflows/build-arm64-portable.yml | |
| with: | |
| assembly_version: ${{ needs.prepare.outputs.assembly_version }} | |
| permissions: | |
| contents: read | |
| # ---- 4. smoke-arm64: windows-11-arm 実機で smoke → 検証済み ZIP+MSI を artifact 化 ---- | |
| smoke-arm64: | |
| needs: [prepare, build-arm64] | |
| if: needs.prepare.outputs.needed == 'true' | |
| runs-on: windows-11-arm | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Resolve arm64 asset names | |
| id: names | |
| shell: pwsh | |
| run: | | |
| $ver = "${{ needs.prepare.outputs.version }}" | |
| "zip_name=CSManager-v.${ver}_arm64.zip" >> $env:GITHUB_OUTPUT | |
| "msi_name=CSManager-setup_arm64.msi" >> $env:GITHUB_OUTPUT | |
| - name: Verify version consistency between prepare and build | |
| shell: pwsh | |
| run: | | |
| if ('${{ needs.prepare.outputs.version }}' -ne '${{ needs.build-arm64.outputs.version }}') { | |
| throw "Version mismatch: prepare=${{ needs.prepare.outputs.version }} build=${{ needs.build-arm64.outputs.version }}" | |
| } | |
| - name: Download portable artifact | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: ${{ needs.build-arm64.outputs.artifact_name }} | |
| path: ${{ runner.temp }}/arm64-artifact | |
| - name: Download arm64 MSI artifact | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: ${{ needs.build-arm64.outputs.msi_artifact_name }} | |
| path: ${{ runner.temp }}/arm64-msi | |
| - name: "Smoke 1: PE machine (CSManager.exe = arm64, no glfw)" | |
| shell: pwsh | |
| run: | | |
| $dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/CSManager" | |
| if (-not (Test-Path (Join-Path $dir "CSManager.exe"))) { throw "Artifact layout unexpected: CSManager.exe not found in $dir" } | |
| if (Test-Path (Join-Path $dir "glfw3.dll")) { throw "glfw3.dll present in arm64 portable (must be excluded, D-CS-2)" } | |
| function Get-PEMachine([string]$path) { | |
| $b = [IO.File]::ReadAllBytes($path) | |
| return [BitConverter]::ToUInt16($b, [BitConverter]::ToInt32($b, 0x3C) + 4) | |
| } | |
| # CSManager は native を持たないため検査対象は exe のみ。 | |
| $m = Get-PEMachine (Join-Path $dir "CSManager.exe") | |
| "CSManager.exe: machine=0x{0:X4}" -f $m | |
| if ($m -ne 0xAA64) { throw "CSManager.exe is not ARM64 (machine=0x$($m.ToString('X4')))" } | |
| - name: "Smoke 2: CSManager.exe --smoke (arch assertion)" | |
| shell: pwsh | |
| run: | | |
| $dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/CSManager" | |
| $out = Join-Path $env:RUNNER_TEMP "smoke-result.txt" | |
| $p = Start-Process -FilePath (Join-Path $dir "CSManager.exe") -ArgumentList @("--smoke", "`"$out`"") -WorkingDirectory $dir -PassThru | |
| if (-not $p.WaitForExit(600000)) { Stop-Process -Id $p.Id -Force; throw "--smoke did not finish within 10 minutes." } | |
| if (-not (Test-Path $out)) { throw "--smoke produced no output file (exit $($p.ExitCode))." } | |
| $result = Get-Content $out -Raw | |
| Write-Host $result | |
| if ($p.ExitCode -ne 0) { throw "--smoke reported failure (exit $($p.ExitCode))." } | |
| if ($result -notmatch [regex]::Escape("arch=Arm64")) { throw "--smoke output missing expected 'arch=Arm64'." } | |
| - name: "Smoke 3: GUI launch (main window appears and survives)" | |
| shell: pwsh | |
| run: | | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public static class WinEnum { | |
| delegate bool EnumProc(IntPtr h, IntPtr lp); | |
| [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr lp); | |
| [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); | |
| [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h); | |
| [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder sb, int n); | |
| [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd); | |
| public static List<string> List(int pid) { | |
| var r = new List<string>(); | |
| EnumWindows((h, lp) => { | |
| uint wpid; GetWindowThreadProcessId(h, out wpid); | |
| if (wpid == pid) { | |
| var sb = new StringBuilder(512); GetWindowText(h, sb, 512); | |
| r.Add((IsWindowVisible(h) ? "visible" : "hidden") + "|" + (GetWindow(h, 4) != IntPtr.Zero ? "owned" : "unowned") + "|" + sb); | |
| } | |
| return true; | |
| }, IntPtr.Zero); | |
| return r; | |
| } | |
| } | |
| '@ | |
| $dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/CSManager" | |
| $p = Start-Process -FilePath (Join-Path $dir "CSManager.exe") -WorkingDirectory $dir -PassThru | |
| $deadline = (Get-Date).AddSeconds(480) | |
| $shown = $false | |
| $lastLog = [DateTime]::MinValue | |
| while ((Get-Date) -lt $deadline) { | |
| if ($p.HasExited) { throw "CSManager.exe exited prematurely (exit $($p.ExitCode))." } | |
| $windows = [WinEnum]::List($p.Id) | |
| if ($windows | Where-Object { $_ -like 'visible|unowned|CSManager*' }) { $shown = $true; break } | |
| if (((Get-Date) - $lastLog).TotalSeconds -ge 30) { | |
| $p.Refresh() | |
| Write-Host ("t={0:f0}s cpu={1:f1}s windows: {2}" -f ((Get-Date) - $p.StartTime).TotalSeconds, $p.TotalProcessorTime.TotalSeconds, ($windows -join ' / ')) | |
| $lastLog = Get-Date | |
| } | |
| Start-Sleep -Seconds 3 | |
| } | |
| if (-not $shown) { | |
| Write-Host "Final window list: $(([WinEnum]::List($p.Id)) -join ' / ')" | |
| Stop-Process -Id $p.Id -Force | |
| throw "CSManager main window did not appear within 480 s." | |
| } | |
| Write-Host "Main window appeared." | |
| Start-Sleep -Seconds 10 | |
| if ($p.HasExited) { throw "CSManager.exe crashed shortly after showing the main window (exit $($p.ExitCode))." } | |
| Stop-Process -Id $p.Id -Force | |
| Write-Host "GUI launch smoke OK." | |
| - name: "Smoke 4: arm64 MSI (per-user install, --smoke, uninstall)" | |
| shell: pwsh | |
| run: | | |
| # MSI の Template/UpgradeCode/ProductVersion 検査はビルド側 (x64 ランナー、build-arm64-portable.yml) で実施済み。 | |
| $msi = Join-Path $env:RUNNER_TEMP "arm64-msi/${{ steps.names.outputs.msi_name }}" | |
| if (-not (Test-Path $msi)) { throw "MSI artifact not found: $msi" } | |
| $hasDesktop = $false | |
| try { $hasDesktop = ((dotnet --list-runtimes) | Select-String "Microsoft.WindowsDesktop.App 10\.") -ne $null } catch {} | |
| if (-not $hasDesktop) { | |
| Write-Host "Installing .NET 10 Desktop Runtime (arm64) for the smoke test..." | |
| Invoke-WebRequest https://dot.net/v1/dotnet-install.ps1 -OutFile "$env:RUNNER_TEMP\dotnet-install.ps1" | |
| & "$env:RUNNER_TEMP\dotnet-install.ps1" -Runtime windowsdesktop -Channel 10.0 -Architecture arm64 -InstallDir "$env:RUNNER_TEMP\dotnet" | |
| $env:DOTNET_ROOT = "$env:RUNNER_TEMP\dotnet" | |
| } | |
| $p = Start-Process msiexec -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', "$env:RUNNER_TEMP\msi-install.log" -Wait -PassThru | |
| if ($p.ExitCode -ne 0) { throw "MSI install failed (exit $($p.ExitCode)). See msi-install.log artifact." } | |
| $appDir = "$env:LOCALAPPDATA\Crystallography Software\CSManager" | |
| if (-not (Test-Path "$appDir\CSManager.exe")) { throw "installed CSManager.exe missing in $appDir" } | |
| $out = Join-Path $env:RUNNER_TEMP "msi-smoke-result.txt" | |
| $p = Start-Process -FilePath "$appDir\CSManager.exe" -ArgumentList @("--smoke", "`"$out`"") -WorkingDirectory $appDir -PassThru | |
| if (-not $p.WaitForExit(600000)) { Stop-Process -Id $p.Id -Force; throw "--smoke (MSI install) did not finish within 10 minutes." } | |
| if (-not (Test-Path $out)) { throw "--smoke produced no output file (exit $($p.ExitCode))." } | |
| $result = Get-Content $out -Raw | |
| Write-Host $result | |
| if ($p.ExitCode -ne 0) { throw "--smoke reported failure (exit $($p.ExitCode))." } | |
| if ($result -notmatch [regex]::Escape("arch=Arm64")) { throw "--smoke output missing expected 'arch=Arm64'." } | |
| $p = Start-Process msiexec -ArgumentList '/x', "`"$msi`"", '/qn', '/l*v', "$env:RUNNER_TEMP\msi-uninstall.log" -Wait -PassThru | |
| if ($p.ExitCode -ne 0) { throw "MSI uninstall failed (exit $($p.ExitCode))." } | |
| if (Test-Path "$appDir\CSManager.exe") { throw "uninstall left CSManager.exe behind" } | |
| Write-Host "MSI smoke OK (install -> --smoke -> uninstall)." | |
| - name: Upload smoke diagnostics on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: smoke-arm64-diagnostics | |
| path: | | |
| ${{ runner.temp }}/smoke-result.txt | |
| ${{ runner.temp }}/msi-smoke-result.txt | |
| ${{ runner.temp }}/msi-install.log | |
| ${{ runner.temp }}/msi-uninstall.log | |
| if-no-files-found: ignore | |
| - name: Create release ZIP and stage arm64 assets | |
| shell: pwsh | |
| run: | | |
| $out = Join-Path $env:RUNNER_TEMP "arm64-out" | |
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |
| $zip = Join-Path $out "${{ steps.names.outputs.zip_name }}" | |
| Compress-Archive -Path (Join-Path $env:RUNNER_TEMP "arm64-artifact/CSManager") -DestinationPath $zip -Force | |
| $msi = Join-Path $env:RUNNER_TEMP "arm64-msi/${{ steps.names.outputs.msi_name }}" | |
| if (-not (Test-Path $msi)) { throw "MSI not found: $msi" } | |
| Copy-Item $msi $out | |
| Get-ChildItem $out | ForEach-Object { Write-Host "arm64 asset: $($_.Name) ($($_.Length) bytes)" } | |
| - name: Upload arm64 release assets | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: arm64-release-assets | |
| path: ${{ runner.temp }}/arm64-out | |
| # ---- 5. release: x64 必須・arm64 は揃っていれば一緒に、1 回の gh release create で原子的に公開 ---- | |
| release: | |
| needs: [prepare, build-x64, build-arm64, smoke-arm64] | |
| if: ${{ !cancelled() && needs.prepare.outputs.needed == 'true' && needs.build-x64.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Download x64 release assets | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: x64-release-assets | |
| path: ${{ runner.temp }}/x64 | |
| - name: Download arm64 release assets | |
| if: ${{ needs.build-arm64.result == 'success' && needs.smoke-arm64.result == 'success' }} | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: arm64-release-assets | |
| path: ${{ runner.temp }}/arm64 | |
| - name: Create tag and GitHub Release | |
| id: create | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DRY_RUN: ${{ inputs.dry_run }} | |
| ARM64_BUILD: ${{ needs.build-arm64.result }} | |
| ARM64_SMOKE: ${{ needs.smoke-arm64.result }} | |
| HISTORY_LINE: ${{ needs.prepare.outputs.history_line }} | |
| run: | | |
| $ver = "${{ needs.prepare.outputs.version }}" | |
| $tag = "${{ needs.prepare.outputs.tag }}" | |
| $historyLine = $env:HISTORY_LINE | |
| $dryRun = $env:DRY_RUN -eq 'true' | |
| $arm64Ok = ($env:ARM64_BUILD -eq 'success') -and ($env:ARM64_SMOKE -eq 'success') | |
| $x64 = Join-Path $env:RUNNER_TEMP "x64" | |
| $assets = @( | |
| (Join-Path $x64 "CSManager-setup.msi") | |
| (Join-Path $x64 "CSManagerSetup.msi") | |
| (Join-Path $x64 "CSManager-v.$ver.zip") | |
| ) | |
| if ($arm64Ok) { | |
| $arm64 = Join-Path $env:RUNNER_TEMP "arm64" | |
| $assets += (Join-Path $arm64 "CSManager-v.${ver}_arm64.zip") | |
| $assets += (Join-Path $arm64 "CSManager-setup_arm64.msi") | |
| $mark = "" | |
| $footnote = @() | |
| } else { | |
| Write-Warning "ARM64 build/smoke did not succeed (build=$env:ARM64_BUILD smoke=$env:ARM64_SMOKE); publishing x64-only with footnote." | |
| $mark = "†" | |
| $footnote = @("", "† Arm64 files are attached after on-device verification and may appear slightly later than the x64 files.") | |
| } | |
| $notes = @( | |
| $historyLine | |
| "" | |
| "| Download | Description |" | |
| "|---|---|" | |
| "| **[CSManager-setup.msi](https://github.com/seto77/CSManager/releases/download/v.$ver/CSManager-setup.msi)** | **Recommended.** Installer for ordinary (x64) Windows PCs. |" | |
| "| [CSManager-setup_arm64.msi](https://github.com/seto77/CSManager/releases/download/v.$ver/CSManager-setup_arm64.msi) | Installer for Windows on Arm (Snapdragon PCs, or Apple Silicon Macs running Windows via virtualization, etc.).$mark |" | |
| "| [CSManager-v.$ver.zip](https://github.com/seto77/CSManager/releases/download/v.$ver/CSManager-v.$ver.zip) | Portable (x64): no installation, self-contained. Suitable for PCs where you have no administrator rights. |" | |
| "| [CSManager-v.${ver}_arm64.zip](https://github.com/seto77/CSManager/releases/download/v.$ver/CSManager-v.${ver}_arm64.zip) | Portable for Windows on Arm: no installation, no administrator rights needed.$mark |" | |
| "| CSManagerSetup.msi | Identical to CSManager-setup.msi (legacy name for auto-update from older versions). |" | |
| ) + $footnote | |
| $notesPath = Join-Path $env:RUNNER_TEMP "release-notes.txt" | |
| $notes | Set-Content -Path $notesPath -Encoding UTF8 | |
| foreach ($a in $assets) { if (-not (Test-Path $a)) { throw "Release asset missing: $a" } } | |
| if ($dryRun) { | |
| Write-Host "=== DRY RUN: would 'gh release create $tag' with assets: ===" | |
| $assets | ForEach-Object { Write-Host " $_" } | |
| Write-Host "=== notes ==="; Get-Content $notesPath | ForEach-Object { Write-Host $_ } | |
| $stage = Join-Path $env:RUNNER_TEMP "release-staged" | |
| New-Item -ItemType Directory -Force -Path $stage | Out-Null | |
| $assets | ForEach-Object { Copy-Item $_ $stage } | |
| Copy-Item $notesPath $stage | |
| "staged=$stage" >> $env:GITHUB_OUTPUT | |
| return | |
| } | |
| # 原子的: tag 未存在時に --target のコミットへ tag を作成し release と一括で作る (orphan tag を防ぐ) | |
| gh release create $tag $assets ` | |
| --target $env:GITHUB_SHA ` | |
| --title "CSManager $tag" ` | |
| --notes-file $notesPath | |
| if ($LASTEXITCODE -ne 0) { throw "gh release create failed (exit $LASTEXITCODE)." } | |
| Write-Host "Released $tag with $($assets.Count) assets (arm64Ok=$arm64Ok)." | |
| - name: Upload staged release assets (dry-run inspection) | |
| if: ${{ inputs.dry_run }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: release-staged-${{ needs.prepare.outputs.tag }} | |
| path: ${{ steps.create.outputs.staged }} |