-
Notifications
You must be signed in to change notification settings - Fork 3
553 lines (510 loc) · 32.9 KB
/
Copy pathrelease.yml
File metadata and controls
553 lines (510 loc) · 32.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# 260406Cl IPAnalyzer 用に PDIndexer の release.yml を改修
# 260602Cl portable ZIP も同じリリースに添付するため、MSI を主配布のまま workflow 名を更新
# 260625Cl WiX 移行 Phase B/C: 旧 vdproj/devenv ビルド経路を全廃し、単一 job を 5 job (prepare/build-x64/build-arm64/
# smoke-arm64/release) に再編。staging publish + dotnet build WiX に置換し、arm64 (portable ZIP + MSI) を同一 run で
# ビルド・windows-11-arm 実機で smoke して x64 と一緒に 1 回の gh release create で原子的に公開する。
# PDIndexer\.github\workflows\release.yml を移植。IPAnalyzer 固有の簡素化:
# * native 依存ゼロ (libxrl も Crystallography.Native も非同梱) → devenv / native ビルド・libxrl 配線・LoadLibrary が全 job で不要
# * glfw3.dll は実行時未使用 (OpenTK 透過の死荷物) → staging/ZIP から除外。arm64 publish には元々入らない
# * arm64 smoke は xraylib 非使用のため --smoke ではなく既存 --capture (全フォーム構築) を機能ゲートに使う
# ★不変条件「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:
- "IPAnalyzer/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 算出 (gh + git だけなので ubuntu) ----
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 "IPAnalyzer/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'
# 同じバージョンのリリースが既にあれば: dry_run は検証目的なので続行 (release job が create をスキップ)、
# force は既存を削除して続行、いずれでもなければスキップ。
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
}
}
# History の先頭行をリリースノートに使う (release job が notes を組む)
$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 値を共有。arm64 へは build-arm64-portable.yml の入力で渡す)。
$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 IPAnalyzer\IPAnalyzer.csproj
# IPAnalyzer は native 依存ゼロのため devenv / native ビルドは無い。
- name: Stage MSI payload (framework-dependent publish)
shell: pwsh
run: |
$s = "artifacts\staging-msi-x64"
dotnet publish IPAnalyzer\IPAnalyzer.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." }
# glfw3.dll を除外 (IPAnalyzer は GLFW/GLControl を生成せず実行時未使用)
Get-ChildItem $s -Filter "glfw3*.dll" -ErrorAction SilentlyContinue | Remove-Item -Force
Copy-Item IPAnalyzerSetup.Wix\LICENSE.rtf, IPAnalyzerSetup.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)" }
if (Test-Path "$s\libxrl-11.dll") { throw "libxrl-11.dll appeared (IPAnalyzer does not bundle xraylib)" }
if (Get-ChildItem $s -Filter "Crystallography.Native*" -ErrorAction SilentlyContinue) { throw "Crystallography.Native* appeared (IPAnalyzer does not bundle it)" }
if (-not (Test-Path "$s\IPAnalyzer.exe")) { throw "IPAnalyzer.exe missing from staging" }
Write-Host "MSI staging OK: $((Get-ChildItem $s -Recurse -File).Count) files"
# 260701Cl 追加: satellite 検査 (10 カルチャ x 2 アセンブリ) を composite action に集約 (旧: インライン $controlsCultures チェック、3ワークフロー6箇所に重複していた)
- name: Verify satellites (x64 staging)
uses: ./.github/actions/verify-satellites
with:
staging-dir: artifacts\staging-msi-x64
label: x64 staging
- name: Build installer with WiX
id: build_installer
timeout-minutes: 10
shell: pwsh
run: |
dotnet build IPAnalyzerSetup.Wix\IPAnalyzerSetup.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)." }
# 主名称 IPAnalyzer-setup.msi + 旧名互換コピー IPAnalyzerSetup.msi (同一バイト、{software}Setup.msi 自動更新固定参照を維持)
$msi = Get-Item "IPAnalyzerSetup.Wix\bin\Release\IPAnalyzer-setup.msi" -ErrorAction SilentlyContinue
if (-not $msi) { throw "IPAnalyzer-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) "IPAnalyzerSetup.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 "IPAnalyzer-portable\IPAnalyzer"
$zipPath = Join-Path (Get-Location) "IPAnalyzer-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 IPAnalyzer\IPAnalyzer.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." }
# glfw3.dll は portable ZIP からも除外 (MSI と同方針)
Get-ChildItem $publishDir -Filter "glfw3*.dll" -ErrorAction SilentlyContinue | Remove-Item -Force
Copy-Item "LICENSE.md" $publishDir -Force
Copy-Item "README-PORTABLE.txt" $publishDir -Force # portable 判定マーカー (MSI には入れない)
Compress-Archive -Path (Join-Path (Split-Path $publishDir -Parent) "IPAnalyzer") -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 化 ----
# このジョブが失敗しても release job は走る (フォールバックで x64 のみ公開)。想定故障は「ビルド緑・実行時死亡」型なので
# PE 検査 + IPAnalyzer.exe --capture (全フォーム構築) + GUI 起動 + MSI install/--capture/uninstall を実機で行う。
smoke-arm64:
needs: [prepare, build-arm64]
if: needs.prepare.outputs.needed == 'true'
runs-on: windows-11-arm # arm64 ネイティブランナー (public repo 無料)
timeout-minutes: 30
steps:
- name: Resolve arm64 asset names
id: names
shell: pwsh
run: |
$ver = "${{ needs.prepare.outputs.version }}"
"zip_name=IPAnalyzer-v.${ver}_arm64.zip" >> $env:GITHUB_OUTPUT
"msi_name=IPAnalyzer-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 + no glfw/native (arm64-native pwsh)"
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/IPAnalyzer"
if (-not (Test-Path (Join-Path $dir "IPAnalyzer.exe"))) { throw "Artifact layout unexpected: IPAnalyzer.exe not found in $dir" }
function Get-PEMachine([string]$path) {
$b = [IO.File]::ReadAllBytes($path)
return [BitConverter]::ToUInt16($b, [BitConverter]::ToInt32($b, 0x3C) + 4)
}
# IPAnalyzer は native 依存ゼロ。glfw/libxrl/Native は不在が正常。
if (Test-Path (Join-Path $dir "glfw3.dll")) { throw "glfw3.dll present in arm64 portable (must be excluded)" }
if (Test-Path (Join-Path $dir "libxrl-11.dll")) { throw "libxrl-11.dll present (IPAnalyzer does not bundle xraylib)" }
if (Get-ChildItem $dir -Filter "Crystallography.Native*" -ErrorAction SilentlyContinue) { throw "Crystallography.Native* present (must not be bundled)" }
$m = Get-PEMachine (Join-Path $dir "IPAnalyzer.exe")
"IPAnalyzer.exe: machine=0x{0:X4}" -f $m
if ($m -ne 0xAA64) { throw "IPAnalyzer.exe is not ARM64 (machine=0x$($m.ToString('X4')))" }
Write-Host "PE machine + no-native check OK."
- name: "Smoke 2: IPAnalyzer.exe --capture (all forms build under arm64)"
shell: pwsh
run: |
# 黙殺フォールバック型故障の検出。--capture は全 parameterless-ctor フォームを reflection で構築する (引数: <出力dir> <culture>)。
# ★機能ゲートの設計 (レビュー指摘反映 260625Cl):
# GuiCapture.Run は全フォーム構築後に無条件 Environment.Exit(0) し、managed なフォーム構築失敗は握り潰して
# _capture-log.tsv の done 行 (ok=N fail=M) に集計するだけ。よって exit code は「ハードクラッシュ (arm64 codegen /
# assembly load 失敗で Exit(0) に到達せず非0)」検出専用で、フォーム構築の成否は反映しない。
# さらに PNG 生成は CopyFromScreen (物理画面取得) 依存で、ヘッドレス/RDP 非表示だと健全でも PNG=0 になる
# (GuiCapture.cs 自身が警告)。そこで機能ゲートは画面非依存な「done 行の fail=0」で行い、PNG 枚数は情報表示のみとする。
# フォーム構築成否 (ok/fail) は画面取得成否と独立に集計される (単色画面でも構築成功なら ok、PNG 保存だけスキップ)。
$dir = Join-Path $env:RUNNER_TEMP "arm64-artifact/IPAnalyzer"
$cap = Join-Path $env:RUNNER_TEMP "smoke2-capture"
New-Item -ItemType Directory -Force $cap | Out-Null
$p = Start-Process -FilePath (Join-Path $dir "IPAnalyzer.exe") -ArgumentList @("--capture", "`"$cap`"", "en") -WorkingDirectory $dir -PassThru
if (-not $p.WaitForExit(600000)) { Stop-Process -Id $p.Id -Force; throw "--capture did not finish within 10 minutes." }
if ($p.ExitCode -ne 0) { throw "--capture crashed (exit $($p.ExitCode)) — hard failure under arm64." }
$logFile = Join-Path $cap "_capture-log.tsv"
if (-not (Test-Path $logFile)) { throw "--capture did not write _capture-log.tsv (GuiCapture.Run did not complete)." }
$doneLine = Get-Content $logFile | Where-Object { $_ -match 'done: ok=(\d+) fail=(\d+)' } | Select-Object -Last 1
if (-not $doneLine) { throw "--capture log has no 'done:' summary (capture aborted mid-run)." }
[void]($doneLine -match 'done: ok=(\d+) fail=(\d+)'); $okCount = [int]$Matches[1]; $failCount = [int]$Matches[2]
$pngs = (Get-ChildItem $cap -Recurse -Filter *.png -ErrorAction SilentlyContinue).Count
Write-Host "capture: ok=$okCount fail=$failCount, $pngs PNG(s) (PNG count is screen-capture dependent, informational)."
if ($failCount -ne 0) {
Write-Host "Failed forms:"; (Get-Content $logFile | Where-Object { $_ -match "`tFAIL`t" }) | ForEach-Object { Write-Host " $_" }
throw "$failCount form(s) failed to construct under arm64."
}
if ($okCount -lt 5) { throw "capture built only $okCount form(s) — form discovery likely failed under arm64." }
- name: "Smoke 3: GUI launch (main window appears and survives)"
shell: pwsh
run: |
# 起動即死の検出。IPAnalyzer は OpenGL を使わない (GuiCapture.cs) ため GL 無効化回避ロジックは不要。
# FormMain_Load は全子フォームを構築してから FormMain を表示し、SetText でタイトルを "IPAnalyzer ver..." にする。
# EnumWindows でプロセスの全トップレベルウィンドウを定期確認し「unowned + visible + タイトル 'IPAnalyzer*'」を合格条件にする。
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/IPAnalyzer"
$p = Start-Process -FilePath (Join-Path $dir "IPAnalyzer.exe") -WorkingDirectory $dir -PassThru
$deadline = (Get-Date).AddSeconds(480) # 初回起動は全子フォーム構築 + JIT で数分かかり得る
$shown = $false
$lastLog = [DateTime]::MinValue
while ((Get-Date) -lt $deadline) {
if ($p.HasExited) { throw "IPAnalyzer.exe exited prematurely (exit $($p.ExitCode))." }
$windows = [WinEnum]::List($p.Id)
if ($windows | Where-Object { $_ -like 'visible|unowned|IPAnalyzer*' }) { $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 ' / ')"
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 "IPAnalyzer 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 "IPAnalyzer.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, --capture, uninstall)"
shell: pwsh
run: |
# MSI の Template/UpgradeCode/ProductVersion 検査はビルド側 (x64 ランナー、build-arm64-portable.yml) で実施済み:
# windows-11-arm の COM 遅延バインディングは OpenDatabase が DISP_E_TYPEMISMATCH で失敗する (ReciPro/PDIndexer 実測)。
# ここでは COM を使わず install/uninstall とも MSI パス指定で行う。
$msi = Join-Path $env:RUNNER_TEMP "arm64-msi/${{ steps.names.outputs.msi_name }}"
if (-not (Test-Path $msi)) { throw "MSI artifact not found: $msi" }
# MSI 版は framework-dependent → smoke 実行に .NET 10 Desktop Runtime (arm64) が必要。ランナーに無ければ導入。
$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\IPAnalyzer"
if (-not (Test-Path "$appDir\IPAnalyzer.exe")) { throw "installed IPAnalyzer.exe missing in $appDir" }
# インストール実体で --capture (黙殺フォールバック型故障の検出。Smoke 2 と同一の画面非依存ゲート: done 行の fail=0)
$cap = Join-Path $env:RUNNER_TEMP "msi-smoke-capture"
New-Item -ItemType Directory -Force $cap | Out-Null
$p = Start-Process -FilePath "$appDir\IPAnalyzer.exe" -ArgumentList @("--capture", "`"$cap`"", "en") -WorkingDirectory $appDir -PassThru
if (-not $p.WaitForExit(600000)) { Stop-Process -Id $p.Id -Force; throw "--capture (MSI install) did not finish within 10 minutes." }
if ($p.ExitCode -ne 0) { throw "--capture (MSI install) crashed (exit $($p.ExitCode)) — hard failure." }
$logFile = Join-Path $cap "_capture-log.tsv"
if (-not (Test-Path $logFile)) { throw "--capture (MSI install) did not write _capture-log.tsv." }
$doneLine = Get-Content $logFile | Where-Object { $_ -match 'done: ok=(\d+) fail=(\d+)' } | Select-Object -Last 1
if (-not $doneLine) { throw "--capture (MSI install) log has no 'done:' summary." }
[void]($doneLine -match 'done: ok=(\d+) fail=(\d+)'); $okCount = [int]$Matches[1]; $failCount = [int]$Matches[2]
$pngs = (Get-ChildItem $cap -Recurse -Filter *.png -ErrorAction SilentlyContinue).Count
Write-Host "MSI-install capture: ok=$okCount fail=$failCount, $pngs PNG(s) (PNG informational)."
if ($failCount -ne 0) {
Write-Host "Failed forms:"; (Get-Content $logFile | Where-Object { $_ -match "`tFAIL`t" }) | ForEach-Object { Write-Host " $_" }
throw "$failCount form(s) failed to construct from MSI install under arm64."
}
if ($okCount -lt 5) { throw "MSI-install capture built only $okCount form(s) — form discovery likely failed." }
# アンインストールしてランナーを汚さない。ProductCode は不要 (MSI パス指定で /x できる)
$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\IPAnalyzer.exe") { throw "uninstall left IPAnalyzer.exe behind" }
Write-Host "MSI smoke OK (install -> --capture -> uninstall)."
- name: Upload smoke diagnostics on failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: smoke-arm64-diagnostics
path: |
${{ runner.temp }}/smoke3-desktop.png
${{ runner.temp }}/msi-install.log
${{ runner.temp }}/msi-uninstall.log
${{ runner.temp }}/smoke2-capture/_capture-log.tsv
${{ runner.temp }}/msi-smoke-capture/_capture-log.tsv
if-no-files-found: ignore
- name: Create release ZIP and stage arm64 assets
shell: pwsh
run: |
# x64 portable ZIP (build-x64) と同じ構造 (ZIP 直下に IPAnalyzer\ フォルダ) に合わせる
$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/IPAnalyzer") -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 }}
# 260625Cl 追加: release job は checkout しない (artifact のみ) ため .git が無く、gh が repo を特定できず
# "failed to run git: fatal: not a git repository" で gh release create が落ちる。GH_REPO で repo を明示する
# (--target は明示 SHA なので API のみで tag+release を作れる。checkout 不要)。
GH_REPO: ${{ github.repository }}
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"
# 主名称 IPAnalyzer-setup.msi + 旧名コピー IPAnalyzerSetup.msi (旧クライアント自動更新互換) + portable ZIP
$assets = @(
(Join-Path $x64 "IPAnalyzer-setup.msi")
(Join-Path $x64 "IPAnalyzerSetup.msi")
(Join-Path $x64 "IPAnalyzer-v.$ver.zip")
)
# arm64 が揃っていれば 5 資産を atomic 公開し †脚注なし。揃わなければ x64 のみ公開し arm64 行は残して †脚注で説明。
if ($arm64Ok) {
$arm64 = Join-Path $env:RUNNER_TEMP "arm64"
$assets += (Join-Path $arm64 "IPAnalyzer-v.${ver}_arm64.zip")
$assets += (Join-Path $arm64 "IPAnalyzer-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 |"
"|---|---|"
"| **[IPAnalyzer-setup.msi](https://github.com/seto77/IPAnalyzer/releases/download/v.$ver/IPAnalyzer-setup.msi)** | **Recommended.** Installer for ordinary (x64) Windows PCs. |"
"| [IPAnalyzer-setup_arm64.msi](https://github.com/seto77/IPAnalyzer/releases/download/v.$ver/IPAnalyzer-setup_arm64.msi) | Installer for Windows on Arm (Snapdragon PCs, or Apple Silicon Macs running Windows via virtualization, etc.).$mark |"
"| [IPAnalyzer-v.$ver.zip](https://github.com/seto77/IPAnalyzer/releases/download/v.$ver/IPAnalyzer-v.$ver.zip) | Portable (x64): no installation, self-contained. Suitable for PCs where you have no administrator rights. |"
"| [IPAnalyzer-v.${ver}_arm64.zip](https://github.com/seto77/IPAnalyzer/releases/download/v.$ver/IPAnalyzer-v.${ver}_arm64.zip) | Portable for Windows on Arm: no installation, no administrator rights needed.$mark |"
"| IPAnalyzerSetup.msi | Identical to IPAnalyzer-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 のコミットへ lightweight tag を作成し release と一括で作る (orphan tag を防ぐ)
gh release create $tag $assets `
--target $env:GITHUB_SHA `
--title "IPAnalyzer $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 }}