Skip to content

Attach ARM64 assets (ZIP + MSI) to release (experimental) #7

Attach ARM64 assets (ZIP + MSI) to release (experimental)

Attach ARM64 assets (ZIP + MSI) to release (experimental) #7

# 260612Cl 追加: win-arm64 portable ZIP を既存 release へ後付け添付する (配布の段階導入の第 2 段階、方針 §6.1-6.2)。
# 260613Cl Phase C: arm64 MSI (WiX、ReciProSetup-arm64.msi) も同時に添付する (WiX計画 §5.2)。
# 段階 1 = build-arm64-portable.yml (artifact のみ。本ワークフローが reusable workflow として再利用する)
# 段階 3 = release.yml の後続 job へ統合 (数リリース安定後)
# 設計の要点 (方針 .project-guidance/ReciPro_ARM64化方針.md):
# - tag は作らない: 既存 release tag へ gh release upload するだけなので、v* tag 削除禁止 ruleset と
# 原子的 release 設計 (260530Cl) に一切干渉しない。x64 配布物にも触れない
# - smoke は upload の前 (windows-11-arm = arm64 ネイティブランナー): 想定故障モードが全て
# 「ビルド緑・実行時死亡」型 (BadImageFormat / DLL 黙殺フォールバック / 起動即死) のため、
# PE 検査 + LoadLibrary + ReciPro.exe --smoke (NativeWrapper/Xraylib 明示アサート) + GUI 起動を実機で行う。
# GL のフル検証は含めない (GLOn12 の flakiness、方針 §6.3。実機検証 = Phase 2 で完了済み)
# - 260613Cl checksum (SHA256SUMS-arm64.txt) の生成・添付を廃止 (checksum 公開は同一チャネルでは
# 改竄対策にならず、リリースページで初心者を惑わすため。作者決定。真正性保証は SignPath 署名で行う)
# - 添付失敗・やり直しは asset の手動削除 (gh release delete-asset <tag> <name>) → 再実行で復旧する
name: Attach ARM64 assets (ZIP + MSI) to release (experimental)
on:
workflow_dispatch:
inputs:
tag:
description: "添付先 release tag (例: v.4.926)。空なら最新 release"
type: string
required: false
default: ""
dry_run:
description: "smoke と ZIP 生成まで実行し、release への upload はしない (artifact で検分可能)"
type: boolean
default: false
permissions:
contents: read
# 同時二重実行による部分アップロード競合を防ぐ
concurrency:
group: attach-arm64-experimental
cancel-in-progress: false
jobs:
# ---- 1. 事前検査: 8 分のビルドを走らせる前に「添付できない理由」を全て洗い出して即失敗させる ----
preflight:
runs-on: ubuntu-latest # 解析と gh API だけなので最速のランナーで (pwsh は ubuntu にもプリインストール)
timeout-minutes: 5
outputs:
tag: ${{ steps.check.outputs.tag }}
version: ${{ steps.check.outputs.version }}
zip_name: ${{ steps.check.outputs.zip_name }}
msi_name: ${{ steps.check.outputs.msi_name }} # 260613Cl Phase C: arm64 MSI
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0 # tag commit の解決と ancestry 検査に全履歴 + tags が必要
- name: Resolve target release and verify preconditions
id: check
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ inputs.dry_run }}
TAG_INPUT: ${{ inputs.tag }} # 式の直接展開ではなく env 経由で渡す (クォート事故防止の定石)
run: |
# dry_run はワークフロー自体の実走テスト用に検査を警告へ緩和する (release 無し・版数不一致でも続行)。
# 本添付 (dry_run=false) は全検査を強制
$dryRun = $env:DRY_RUN -eq 'true'
# 本添付は master からのみ許可 (release 資産は master の歴史と対応するため)
if ($env:GITHUB_REF_NAME -ne 'master' -and -not $dryRun) {
throw "Real attach must run from master (current: $env:GITHUB_REF_NAME). Use dry_run for branch testing."
}
# release.yml と同じ規則で Version.cs からバージョンを得る
$content = Get-Content "ReciPro/Version.cs" -Raw
if ($content -notmatch 'ver(\d+\.\d+)\(') {
throw "Could not parse version from Version.cs History."
}
$ver = $Matches[1]
# 添付先 tag: 入力が空なら最新 release
$tag = $env:TAG_INPUT
if (-not $tag) {
$tag = (gh release view --json tagName -q '.tagName')
if ($LASTEXITCODE -ne 0 -or -not $tag) {
$global:LASTEXITCODE = 0
if ($dryRun) { $tag = "v.$ver"; Write-Warning "No release found; dry_run continues with synthesized tag '$tag' (used for naming only)." }
else { throw "No existing release found (and no tag input given)." }
}
}
# release の存在確認 (入力 tag の typo もここで止まる)
gh release view $tag --json name | Out-Null
$releaseExists = ($LASTEXITCODE -eq 0)
$global:LASTEXITCODE = 0
if (-not $releaseExists -and -not $dryRun) { throw "Release '$tag' does not exist." }
# ビルドされる exe / ZIP 名のバージョンと添付先 release の版数一致を強制
# (古い release に新しいバイナリを付ける事故、およびその逆を防ぐ)
if ("v.$ver" -ne $tag) {
if ($dryRun) { Write-Warning "Version mismatch: Version.cs=v.$ver, target=$tag (dry_run continues)." }
else { throw "Version mismatch: Version.cs says v.$ver but target release is $tag. Attach only to the matching release." }
}
if ($releaseExists) {
# ancestry 検査 (codex レビュー指摘): 添付する arm64 ZIP は「release 時点の commit」ではなく
# 「今の master (GITHUB_SHA)」からビルドされる。arm64 ビルド基盤が release 後に入ったため
# tag からのビルドは不可能で、これは backfill の宿命として受容する。その代わり
# ① tag の commit がビルド commit の祖先であること (別系列の歴史への添付を拒否)
# ② 出所 (両 SHA) を step summary に明記すること
# で「同名バージョンの無関係バイナリ」を防ぐ。段階 3 (release.yml 統合) では同一 run ビルドになり自然解消
$tagSha = (git rev-list -n 1 $tag 2>$null)
if ($LASTEXITCODE -ne 0 -or -not $tagSha) { throw "Tag '$tag' was not found in the git history." }
git merge-base --is-ancestor $tagSha $env:GITHUB_SHA
if ($LASTEXITCODE -ne 0) { throw "Release tag commit $tagSha is not an ancestor of build commit $($env:GITHUB_SHA)." }
@(
"## ARM64 attach provenance"
"- Target release: $tag (commit $tagSha)"
"- ARM64 ZIP built from: $($env:GITHUB_SHA) ($($env:GITHUB_REF_NAME))"
if ($tagSha -ne $env:GITHUB_SHA) { "- Note: the ARM64 binary includes commits made after the release (experimental backfill)." }
) | Add-Content $env:GITHUB_STEP_SUMMARY
}
# asset 衝突検査: --clobber による差し替えはしない方針のため、既存なら手動削除を促して失敗
# 260613Cl 作者指示で命名簡素化 (旧: ReciPro-v.X-win-arm64-experimental-portable.zip)。checksum は公開廃止
# 260613Cl Phase C: arm64 MSI (WiX) も添付対象に追加
$zipName = "ReciPro-v.$ver-arm64.zip"
$msiName = "ReciProSetup-arm64.msi"
if ($releaseExists) {
$assets = @(gh release view $tag --json assets -q '.assets[].name')
foreach ($name in @($zipName, $msiName)) {
if ($assets -contains $name) {
if ($dryRun) { Write-Warning "Asset '$name' already exists on $tag (dry_run continues)." }
else { throw "Asset '$name' already exists on $tag. Delete it manually first (gh release delete-asset $tag $name) and re-run." }
}
}
}
Write-Host "Target: $tag / version $ver / zip $zipName / msi $msiName"
"tag=$tag" >> $env:GITHUB_OUTPUT
"version=$ver" >> $env:GITHUB_OUTPUT
"zip_name=$zipName" >> $env:GITHUB_OUTPUT
"msi_name=$msiName" >> $env:GITHUB_OUTPUT
# ---- 2. ビルド: 段階 1 のワークフローをそのまま再利用 (検査込み。二重保守を作らない) ----
build:
needs: preflight
uses: ./.github/workflows/build-arm64-portable.yml
permissions:
contents: read
# ---- 3. arm64 実機 smoke → 合格時のみ release へ添付 ----
smoke-and-attach:
needs: [preflight, build]
runs-on: windows-11-arm # arm64 ネイティブランナー (public repo 無料、方針 §1.3)
timeout-minutes: 30
permissions:
contents: write # gh release upload に必要 (他 job は read のまま)
steps:
- name: Checkout
uses: actions/checkout@v5 # native\win-arm64\ の LoadLibrary 検査用 (バンドル内 DLL と同一ソース)
- name: Verify version consistency between preflight and build
shell: pwsh
run: |
# 同一 SHA から解析しているので一致するはずだが、出力配線のミスをここで検出する
if ('${{ needs.preflight.outputs.version }}' -ne '${{ needs.build.outputs.version }}') {
throw "Version mismatch: preflight=${{ needs.preflight.outputs.version }} build=${{ needs.build.outputs.version }}"
}
- name: Download build artifact
# v7 = upload-artifact@v6 と同日リリースの Node 24 対 (download 側は 2025-08 の v5 breaking の分 major が 1 進んでいる)。
# 同一 run 内 (reusable workflow がアップロードした artifact を含む) は name 指定だけで取得できる
uses: actions/download-artifact@v7
with:
name: ${{ needs.build.outputs.artifact_name }}
path: ${{ runner.temp }}/arm64-artifact
- name: Download arm64 MSI artifact # 260613Cl Phase C
uses: actions/download-artifact@v7
with:
name: ${{ needs.build.outputs.msi_artifact_name }}
path: ${{ runner.temp }}/arm64-msi
- name: "Smoke 1: PE machine + native LoadLibrary (arm64-native pwsh)"
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/ReciPro"
if (-not (Test-Path (Join-Path $dir "ReciPro.exe"))) {
throw "Artifact layout unexpected: ReciPro.exe not found in $dir"
}
# PE machine 再検査 (ビルド側でも検査済みだが、artifact 往復後の実物を添付直前に確認する)
function Get-PEMachine([string]$path) {
$b = [IO.File]::ReadAllBytes($path)
$pe = [BitConverter]::ToInt32($b, 0x3C)
return [BitConverter]::ToUInt16($b, $pe + 4)
}
foreach ($f in @("ReciPro.exe", "Crystallography.Native.dll")) {
$m = Get-PEMachine (Join-Path $dir $f)
"{0}: machine=0x{1:X4}" -f $f, $m
if ($m -ne 0xAA64) { throw "$f is not ARM64 (machine=0x$($m.ToString('X4')))" }
}
# arm64 ネイティブの pwsh で実 LoadLibrary (= AA64 PE + /MT 静的 CRT がクリーン環境で本当にロードできるか)。
# libxrl/glfw のリポ側コピーはここで検査し、単一ファイル exe バンドル内の実物は後続の --smoke が検査する
# (xraylib self-test + glfwInit。リポ側だけでは配布物の検査にならない、codex レビュー指摘)
foreach ($dll in @(
(Join-Path $dir "Crystallography.Native.dll"),
"ReciPro\native\win-arm64\libxrl-11.dll",
"ReciPro\native\win-arm64\glfw3.dll")) {
$abs = (Resolve-Path $dll).Path
$h = [System.Runtime.InteropServices.NativeLibrary]::Load($abs)
[System.Runtime.InteropServices.NativeLibrary]::Free($h)
Write-Host "LoadLibrary OK: $abs"
}
- name: "Smoke 2: ReciPro.exe --smoke (NativeWrapper / Xraylib explicit assertion)"
shell: pwsh
run: |
# 黙殺フォールバック型 (動くが native 無効で異常に遅い) を CI で確実に検出する (方針 §7)。
# 初回起動は単一ファイルバンドルの抽出 + Defender スキャンで時間がかかるためタイムアウトは長めに
$dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/ReciPro"
$out = Join-Path $env:RUNNER_TEMP "smoke-result.txt"
# 引数はパスに空白が入っても壊れないよう明示的にクォートする (Start-Process は自動クォートしない)
$p = Start-Process -FilePath (Join-Path $dir "ReciPro.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)). See output above." }
# glfwEnabled は単一ファイルバンドル内の glfw3.dll を実際に抽出・glfwInit して検査した結果
foreach ($expect in @("arch=Arm64", "nativeEnabled=True", "xraylibEnabled=True", "glfwEnabled=True")) {
if ($result -notmatch [regex]::Escape($expect)) { throw "--smoke output missing expected '$expect'." }
}
- name: "Smoke 3: GUI launch (main window appears and survives)"
shell: pwsh
run: |
# 起動即死 (例外ダイアログ前のクラッシュ等) の検出。ウィンドウ生成までを確認して終了する。
# GL 検証はしない (方針 §6.3): GPU 無しの hosted runner は GDI Generic GL 1.1 のみで、
# GLControlAlpha の静的初期化 (checkSupportedVersion = GL コンテキスト実生成) が WndProc 内例外
# → ThreadExceptionDialog ("Microsoft .NET") のモーダル表示で永久ブロックする (run 27383602242/27384362544 で実証)。
# CRYSTALLOGRAPHY_DISABLE_OPENGL=1 (260612Cl GLControlAlpha + FormMain) で GL 判定を丸ごとスキップし
# 3D 無効モードの起動経路を検査する。GL の実機検証は Phase 2 (Surface) で完了済み、
# バンドル内 glfw3.dll のロードは Smoke 2 (glfwInit) が検査済み
# 260612Cl 初回 dry_run (run 27382975111) の知見で改訂:
# - FormMain_Load は進捗ダイアログ (Owner 付き = Process.MainWindowHandle に数えられない) を出した後、
# 全子フォームを構築してから FormMain を表示する。Process.MainWindowHandle 頼みでは進捗が見えない
# - → EnumWindows でプロセスの全トップレベルウィンドウ (hidden/owned 含む) と CPU 時間を定期ログし、
# 「unowned + visible + タイトル 'ReciPro*'」(= Load 完了後の FormMain) を合格条件にする。
# タイトル検査はエラーダイアログの誤合格防止 (codex レビュー指摘) を兼ねる
# - タイムアウト時はデスクトップをスクリーンショットして artifact 化 (次ステップ)
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/ReciPro"
$env:CRYSTALLOGRAPHY_DISABLE_OPENGL = "1" # 上記コメント参照 (タイトルは "ReciPro ... (3D rendering disable mode)" になる)
$p = Start-Process -FilePath (Join-Path $dir "ReciPro.exe") -WorkingDirectory $dir -PassThru
$deadline = (Get-Date).AddSeconds(480) # 初回起動は全子フォーム構築 + JIT で CI の非力な VM では数分かかり得る
$shown = $false
$lastLog = [DateTime]::MinValue
while ((Get-Date) -lt $deadline) {
if ($p.HasExited) { throw "ReciPro.exe exited prematurely (exit $($p.ExitCode))." }
$windows = [WinEnum]::List($p.Id)
if ($windows | Where-Object { $_ -like 'visible|unowned|ReciPro*' }) { $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) {
# 失敗診断: デスクトップのスクリーンショット (hosted runner は interactive desktop session) と最終ウィンドウ一覧
Write-Host "Final window list: $(([WinEnum]::List($p.Id)) -join ' / ')"
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
[System.Drawing.Graphics]::FromImage($bmp).CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bmp.Size)
$bmp.Save((Join-Path $env:RUNNER_TEMP "smoke3-desktop.png"))
Stop-Process -Id $p.Id -Force
throw "ReciPro main window did not appear within 480 s (see logged window lists and smoke3-desktop.png artifact)."
}
Write-Host "Main window appeared: $(([WinEnum]::List($p.Id) | Where-Object { $_ -like 'visible|*' }) -join ' / ')"
Start-Sleep -Seconds 10 # 表示直後のクラッシュ検出
if ($p.HasExited) { throw "ReciPro.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 (Template check, per-user install, --smoke, uninstall)" # 260613Cl Phase C
shell: pwsh
run: |
# MSI の Template/プロパティ検査 (x64 機での誤実行は Windows Installer が拒否するが、配布前に自前でも確認)
$msi = Join-Path $env:RUNNER_TEMP "arm64-msi/${{ needs.preflight.outputs.msi_name }}"
if (-not (Test-Path $msi)) { throw "MSI artifact not found: $msi" }
$wi = New-Object -ComObject WindowsInstaller.Installer
$db = $wi.GetType().InvokeMember('OpenDatabase','InvokeMethod',$null,$wi,@($msi,0))
$si = $db.GetType().InvokeMember('SummaryInformation','GetProperty',$null,$db,@(0))
$template = $si.GetType().InvokeMember('Property','GetProperty',$null,$si,@(7))
Write-Host "MSI Template = $template"
if ($template -notlike 'Arm64*') { throw "MSI Template is not Arm64: $template" }
function Get-MsiProp($name) {
$view = $db.GetType().InvokeMember('OpenView','InvokeMethod',$null,$db,@("SELECT Value FROM Property WHERE Property='$name'"))
$view.GetType().InvokeMember('Execute','InvokeMethod',$null,$view,$null) | Out-Null
$rec = $view.GetType().InvokeMember('Fetch','InvokeMethod',$null,$view,$null)
if ($rec) { $rec.GetType().InvokeMember('StringData','GetProperty',$null,$rec,@(1)) } else { $null }
}
$uc = Get-MsiProp 'UpgradeCode'
if ($uc -ne '{A81303DC-E1CA-44F3-AE3A-6C7CAC08E8D8}') { throw "UpgradeCode mismatch: $uc" }
$pv = Get-MsiProp 'ProductVersion'
if ($pv -ne '0.${{ needs.preflight.outputs.version }}') { throw "ProductVersion mismatch: $pv" }
# MSI 版は framework-dependent → smoke 実行に .NET 10 Desktop Runtime (arm64) が必要。
# ランナーに無ければ dotnet-install で導入 (DOTNET_ROOT は同一ステップ内の子プロセスに引き継がれる)
$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"
}
# per-user silent install (ALLUSERS/MSIINSTALLPERUSER は渡さない = per-user 固定パッケージの既定)
$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\ReciPro"
if (-not (Test-Path "$appDir\ReciPro.exe")) { throw "installed ReciPro.exe missing in $appDir" }
# インストール実体で --smoke (黙殺フォールバック型故障の検出。Smoke 2 と同一アサート)
$out = Join-Path $env:RUNNER_TEMP "msi-smoke-result.txt"
$p = Start-Process -FilePath "$appDir\ReciPro.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))." }
foreach ($expect in @("arch=Arm64", "nativeEnabled=True", "xraylibEnabled=True", "glfwEnabled=True")) {
if ($result -notmatch [regex]::Escape($expect)) { throw "--smoke output missing expected '$expect'." }
}
# アンインストールしてランナーを汚さない (残骸検査込み)
$code = Get-MsiProp 'ProductCode'
$p = Start-Process msiexec -ArgumentList '/x', $code, '/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\ReciPro.exe") { throw "uninstall left ReciPro.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: smoke3-diagnostics
path: |
${{ runner.temp }}/smoke3-desktop.png
${{ 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
id: assets
shell: pwsh
run: |
# x64 portable ZIP (release.yml) と同じ構造 (ZIP 直下に ReciPro\ フォルダ) に合わせる
# 260613Cl checksum (SHA256SUMS-arm64.txt) の生成を廃止
$zip = Join-Path $env:RUNNER_TEMP "${{ needs.preflight.outputs.zip_name }}"
Compress-Archive -Path (Join-Path $env:RUNNER_TEMP "arm64-artifact/ReciPro") -DestinationPath $zip -Force
"zip=$zip" >> $env:GITHUB_OUTPUT
# 260613Cl Phase C: smoke 済み MSI も添付対象に
$msi = Join-Path $env:RUNNER_TEMP "arm64-msi/${{ needs.preflight.outputs.msi_name }}"
if (-not (Test-Path $msi)) { throw "MSI not found: $msi" }
"msi=$msi" >> $env:GITHUB_OUTPUT
- name: Upload release assets as workflow artifact (inspection record)
# dry_run の検分用 + 実添付時の記録。release に上がる実物と bit 同一
uses: actions/upload-artifact@v6
with:
name: release-assets-${{ needs.preflight.outputs.tag }}-win-arm64-experimental
path: |
${{ steps.assets.outputs.zip }}
${{ steps.assets.outputs.msi }}
- name: Attach to release
if: ${{ !inputs.dry_run }}
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = '${{ needs.preflight.outputs.tag }}'
# preflight から時間が経っているため、直前にもう一度 asset 衝突を検査する
$assets = @(gh release view $tag --json assets -q '.assets[].name')
foreach ($name in @('${{ needs.preflight.outputs.zip_name }}', '${{ needs.preflight.outputs.msi_name }}')) {
if ($assets -contains $name) {
throw "Asset '$name' appeared on $tag after preflight. Delete it manually (gh release delete-asset $tag $name) and re-run."
}
}
# --clobber は使わない (方針 §6.2)。部分失敗時は手動で delete-asset → 再実行
# 260613Cl checksum の添付は廃止。Phase C: ZIP + MSI を添付
gh release upload $tag "${{ steps.assets.outputs.zip }}" "${{ steps.assets.outputs.msi }}"
if ($LASTEXITCODE -ne 0) { throw "gh release upload failed (exit $LASTEXITCODE)." }
Write-Host "Attached to https://github.com/${{ github.repository }}/releases/tag/$tag"