name: Windows MSI # 태그(v*) 푸시는 MSI 를 같은 GitHub Release 에 첨부한다 (linux-deb / linux-rpm # 과 같은 릴리스 페이지). # # push 에는 paths 필터를 두지 않는다 — paths 는 푸시에 포함된 커밋의 변경 파일로 # 판정되는데, 기존 커밋을 가리키는 태그 푸시에는 변경 파일이 없어 릴리스 빌드가 # 통째로 스킵될 수 있다. 릴리스 경로에 그런 모호함을 남기지 않으려고 브랜치 # 푸시도 항상 빌드한다(공개 저장소라 러너 비용 없음). PR 에는 그대로 paths 를 # 적용해 무관한 PR 에서 45분 잡이 도는 것만 막는다. on: push: tags: ['v*'] branches: [main, develop] pull_request: paths: - 'unim-tsf/**' - 'unim-imm32/**' - 'unim-windows-common/**' - 'unim-popup-win/**' - 'unim-settings/**' - 'unim-capi/**' - 'installer/**' - 'help/**' - 'src/**' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/windows-msi.yml' # 설치·등록·기능 실측 스크립트 — 이 파일만 고쳐도 잡이 돌아야 한다. # (push 쪽에는 paths 필터가 없다 — 위 주석의 태그 스킵 방지 정책 그대로.) - 'scripts/ci/verify-msi.ps1' workflow_dispatch: env: CARGO_TERM_COLOR: always # 상시 게이트(브랜치·PR)에서는 경고를 오류로 승격하지만, 태그 빌드는 이미 # 태그된 코드를 '그대로' 내보내야 하므로 승격하지 않는다 — 경고 하나로 릴리스가 # 막히는 사고 방지 (linux-deb.yml 이 같은 이유로 -Dwarnings 를 뺀 것과 동일 정책). RUSTFLAGS: ${{ startsWith(github.ref, 'refs/tags/v') && '' || '-Dwarnings' }} WIN_TARGET: x86_64-pc-windows-msvc # 브랜치 push·PR 은 새 커밋이 오면 이전 실행을 취소한다(최신 커밋만 의미 있음). # 태그(릴리스)는 취소하지 않고 끝까지 완주시킨다. concurrency: group: windows-msi-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }} jobs: build-msi: name: Build MSVC + WiX MSI runs-on: windows-2022 timeout-minutes: 60 permissions: contents: write # 태그 빌드에서 softprops/action-gh-release 가 자산 첨부에 사용 steps: - uses: actions/checkout@v4 - name: Verify GUID/version sync (gen-guids.sh) shell: bash run: | bash installer/wix/gen-guids.sh if ! git diff --exit-code installer/wix/generated/guids.wxi; then echo "::error::installer/wix/generated/guids.wxi out of sync with unim-tsf/src/globals.rs" echo "::error::Run 'bash installer/wix/gen-guids.sh' locally and commit the result." exit 1 fi - name: Install Rust toolchain (stable, MSVC) uses: dtolnay/rust-toolchain@stable with: targets: x86_64-pc-windows-msvc,i686-pc-windows-msvc - name: Cargo cache uses: Swatinem/rust-cache@v2 with: shared-key: windows-msvc cache-targets: true cache-on-failure: true - name: cargo check (Windows crates, MSVC target) # --workspace 는 불가: gtk/cairo-sys 등 Linux 전용 GUI 크레이트가 # Windows 타깃으로 cross-compile 되지 않는다. build 스텝과 동일하게 # Windows 대상 크레이트(+코어 의존)만 검증한다. run: | cargo check --target ${{ env.WIN_TARGET }} --locked ` -p unim ` -p unim-capi ` -p unim-tsf ` -p unim-imm32 ` -p unim-settings - name: cargo test (core + native Windows crates) # 릴리스 출하 전 동작 검증 게이트. 코어(unim) + MSVC 타깃으로 # 이미 cargo check 되는 unim-capi/unim-tsf + 순수 유닛테스트만 # 있는 unim-windows-common/unim-popup-win 포함. # unim-keymap-common 은 gtk4/libadwaita(Linux GUI) 의존이라 제외 # (MSVC 타깃 크로스컴파일 불가). # (COM/GUI 인스턴스화·레지스트리 실기록 테스트 없음 — 헤드리스 러너 안전.) run: | cargo test --target ${{ env.WIN_TARGET }} --locked ` -p unim ` -p unim-capi ` -p unim-tsf ` -p unim-windows-common ` -p unim-popup-win - name: cargo build (release, MSI payloads + IMM32 diagnostic build) # MSI 가 실제로 패키징하는 산출물: TSF DLL + 설정 GUI + 팝업 렌더러 (x64). # unim-imm32 는 MSI 미탑재(진단·연구용 빌드) — 여기서는 unim_imm32 크레이트가 # 계속 컴파일되는지만 확인한다. `.ime` 부재는 아래 검증 스텝에서 경고일 뿐 # 릴리스 게이트를 막지 않는다(M-18 — README.md:88 등 문서와 서술 일치). run: | cargo build --release --target ${{ env.WIN_TARGET }} ` -p unim-tsf ` -p unim-settings ` -p unim-popup-win ` -p unim-imm32 ` --locked - name: cargo build (release, 32-bit payloads — SysWOW64) # unim_tsf.dll 의 32-bit 사본만 MSI 에 실제 패키징된다 # (unim.wxs UnimTsfDll32 컴포넌트가 WOW6432Node CLSID 뷰용으로 담는다). # unim_imm32 의 32-bit 빌드는 MSI 미탑재 — 컴파일 확인용 진단 빌드다. # 32-bit 환경 없이 --target i686 을 넘기면 링크 오류가 나므로 # vcvarsamd64_x86 을 먼저 호출하는 별도 셸에서 실행한다. shell: cmd run: | call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsamd64_x86.bat" >nul cargo build --release --target i686-pc-windows-msvc -p unim-imm32 -p unim-tsf --locked if errorlevel 1 exit /b 1 - name: Rename IMM32 DLL → IME (both bitnesses) shell: bash run: | cp target/${WIN_TARGET}/release/unim_imm32.dll \ target/${WIN_TARGET}/release/unim_imm32.ime cp target/i686-pc-windows-msvc/release/unim_imm32.dll \ target/i686-pc-windows-msvc/release/unim_imm32.ime - name: Verify build artifacts # M-18: unim.wxs 에는 unim_imm32/.ime 문자열이 0건이다 — IMM32 는 MSI 에 # 들어가지 않는 진단·연구용 빌드일 뿐이다. 이 스텝은 실제로 MSI 에 # 패키징되는 산출물(TSF DLL x64/x86, 설정 GUI, 팝업 렌더러)만 부재 시 # 릴리스 잡을 실패시키고, IMM32 `.ime` 부재는 경고로만 남긴다. shell: bash run: | out64="target/${WIN_TARGET}/release" out32="target/i686-pc-windows-msvc/release" missing=0 for f in unim_tsf.dll unim-settings.exe unim-popup-win.exe; do if [[ ! -f "$out64/$f" ]]; then echo "::error::missing $out64/$f" missing=1 else size=$(stat -c%s "$out64/$f") echo " ok $out64/$f ($size bytes)" fi done if [[ ! -f "$out32/unim_tsf.dll" ]]; then echo "::error::missing $out32/unim_tsf.dll" missing=1 else size=$(stat -c%s "$out32/unim_tsf.dll") echo " ok $out32/unim_tsf.dll ($size bytes)" fi # IMM32 는 진단·연구용 빌드일 뿐 MSI 에 담기지 않는다 — 부재는 경고만. for f in "$out64/unim_imm32.ime" "$out32/unim_imm32.ime"; do if [[ ! -f "$f" ]]; then echo "::warning::missing $f (unim-imm32 is a diagnostic-only build, not packaged in the MSI)" else size=$(stat -c%s "$f") echo " ok $f ($size bytes, diagnostic-only)" fi done [[ $missing -eq 0 ]] - name: Read workspace version id: version shell: bash run: | v=$(grep -E '^version *= *"' Cargo.toml | head -1 | sed -E 's/.*"([0-9.]+)".*/\1/') echo "version=$v" >> "$GITHUB_OUTPUT" echo "UNIM version: $v" - name: Locate WiX Toolset (v3) id: wix shell: pwsh run: | $candle = (Get-Command candle.exe -ErrorAction SilentlyContinue)?.Source if (-not $candle -and $env:WIX) { $candle = Join-Path $env:WIX 'bin\candle.exe' } if (-not (Test-Path $candle)) { Write-Error "candle.exe not found. WiX Toolset v3 must be available on the runner." } $bin = Split-Path $candle -Parent "wix_bin=$bin" >> $env:GITHUB_OUTPUT Write-Host "WiX bin: $bin" & "$bin\candle.exe" -? | Select-Object -First 3 - name: Build MSI (candle + light) shell: pwsh run: | $winOut = "target\${env:WIN_TARGET}\release" $dist = "dist" New-Item -ItemType Directory -Path $dist -Force | Out-Null $candle = "${{ steps.wix.outputs.wix_bin }}\candle.exe" $light = "${{ steps.wix.outputs.wix_bin }}\light.exe" $winOut32 = "target\i686-pc-windows-msvc\release" & $candle ` -arch x64 ` -ext WixUtilExtension ` -dWIN_OUT_DIR="$winOut" ` -dWIN_OUT_DIR32="$winOut32" ` -out "installer\wix\unim.wixobj" ` "installer\wix\unim.wxs" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $msi = "$dist\unim-${{ steps.version.outputs.version }}-x64.msi" & $light ` -sval ` -ext WixUtilExtension ` -out "$msi" ` "installer\wix\unim.wixobj" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Write-Host "Built: $msi" Get-Item $msi | Select-Object Name, Length, LastWriteTime # PKG-WIN-V3: 이름이 예전엔 "light -sval covers ICE" 라 했으나 사실과 반대다 — # `light -sval` 은 ICE 검증을 끈다(위 Build MSI 스텝). 이 스텝은 ICE 를 전혀 # 확인하지 않고, 산출물 존재·최소 크기만 본다. `-sval` 제거는 보류한다 — CI # 왕복 검증이 불가한 상태에서 실패하는 ICE 를 모른 채 건드리면 태깅 당일 MSI # 빌드가 처음 실패할 위험이 있다(2026-07-27 사전 리스크 분석 §5-4 결정). - name: MSI sanity check (file exists + min size — NOT an ICE/structural validation; light -sval disables ICE) shell: pwsh run: | $msi = "dist\unim-${{ steps.version.outputs.version }}-x64.msi" if (-not (Test-Path $msi)) { Write-Error "MSI not produced"; exit 1 } $size = (Get-Item $msi).Length if ($size -lt 100000) { Write-Error "MSI suspiciously small: $size bytes"; exit 1 } Write-Host "MSI size OK: $size bytes" # ── SignPath Foundation Authenticode 서명 (시크릿/변수 없으면 조용히 스킵) ── # # 2026-09-03 Defender 오탐(Bearfoos.B!ml) 사고 대응 3축(서명·VERSIONINFO· # Defender 능동 스캔) 중 서명 축. SignPath Foundation 은 OSS 프로젝트용 # 무료 Authenticode 서명(인증서 주체 = "SignPath Foundation"). 신청·설정 # 절차는 docs/dev/windows/CODE_SIGNING.md. organization-id/project-slug 는 # 민감정보가 아니라 vars(Repository variables)로 두고, api-token 만 # secrets 로 둔다 — SignPath 공식 예제(signpath/github-action-submit- # signing-request README)도 앞의 둘을 워크플로에 평문으로 적는다. # # 셋 중 하나라도 비어 있으면(미신청 상태) 아래 블록 전체를 조용히 # 건너뛴다 — 실패가 아니라 스킵. fork PR 은 secrets/vars 가 원천적으로 # 비므로 안전하고, 같은 저장소 PR 도 명시적으로 제외한다(승인 대기로 # PR 마다 블로킹되는 걸 막는다 — DEPLOY-TRUST-PLAN.md a-4 스켈레톤의 # `if: github.event_name != 'pull_request'` 선례와 동일 방침). - name: Check SignPath configuration id: signpath-config shell: bash run: | if [[ "${{ github.event_name }}" != "pull_request" \ && -n "${{ vars.SIGNPATH_ORGANIZATION_ID }}" \ && -n "${{ vars.SIGNPATH_PROJECT_SLUG }}" \ && -n "${{ secrets.SIGNPATH_API_TOKEN }}" ]]; then echo "enabled=true" >> "$GITHUB_OUTPUT" else echo "enabled=false" >> "$GITHUB_OUTPUT" echo "::notice::SignPath 시크릿/변수 미설정(또는 PR 빌드) — 서명 단계를 건너뜁니다. 신청 절차: docs/dev/windows/CODE_SIGNING.md" fi # 서명 요청 제출용 미서명 MSI — 최종 게시 아티팩트(아래 'Upload MSI # artifact')와 이름이 겹치지 않도록 '-unsigned' 접미사, 짧은 보존기간 # (전달용 중간 산출물이라 30일씩 남길 이유가 없다). - name: Upload unsigned MSI for signing id: upload-unsigned-msi if: steps.signpath-config.outputs.enabled == 'true' uses: actions/upload-artifact@v4 with: name: unim-${{ steps.version.outputs.version }}-x64-msi-unsigned path: dist/unim-${{ steps.version.outputs.version }}-x64.msi if-no-files-found: error retention-days: 1 - name: Determine SignPath signing policy id: signpath-policy if: steps.signpath-config.outputs.enabled == 'true' shell: bash run: | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then echo "slug=release-signing" >> "$GITHUB_OUTPUT" else echo "slug=test-signing" >> "$GITHUB_OUTPUT" fi echo "Signing policy: $(cat "$GITHUB_OUTPUT" | tail -1)" # SignPath Foundation OSS 정책상 모든 서명 요청은 프로젝트 승인자 # (Approver)의 수동 승인이 필요하다(signpath.org/terms.html "Every # release needs manual approval for signing"). 승인이 늦거나 없으면 # wait-for-completion 이 타임아웃될 수 있으므로 continue-on-error 로 # 두고, 실패/타임아웃 시 아래 단계가 미서명 MSI 로 자동 폴백한다 — # 태그 릴리스만큼은 실제로 승인해 release-signing 을 받아야 서명본이 # 나온다(승인 없이 태그를 그대로 배포하면 여전히 무서명 MSI 가 릴리스에 # 첨부된다 — 이 경우 로그의 ::warning:: 을 놓치지 말 것). - name: Submit SignPath signing request id: signpath if: steps.signpath-config.outputs.enabled == 'true' continue-on-error: true timeout-minutes: 12 uses: signpath/github-action-submit-signing-request@v2 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: '${{ vars.SIGNPATH_ORGANIZATION_ID }}' project-slug: '${{ vars.SIGNPATH_PROJECT_SLUG }}' signing-policy-slug: '${{ steps.signpath-policy.outputs.slug }}' artifact-configuration-slug: '${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}' github-artifact-id: '${{ steps.upload-unsigned-msi.outputs.artifact-id }}' wait-for-completion: true output-artifact-directory: 'msi-signed' # dist\unim--x64.msi 를 서명본으로 교체 — 이 파일 경로를 그대로 # 쓰는 아래 모든 단계(Defender scan·설치 검증·SHA256SUMS·릴리스 첨부)가 # 별도 수정 없이 서명본을 쓰게 된다. 서명 스텝이 스킵/실패했으면 # outcome 이 'success' 가 아니므로 이 스텝도 no-op — dist 안의 기존 # 미서명 MSI 가 그대로 이후 단계로 흘러간다(회귀 없음). - name: Replace MSI with SignPath-signed version id: signed-msi if: steps.signpath.outcome == 'success' shell: bash run: | set -euo pipefail msi="dist/unim-${{ steps.version.outputs.version }}-x64.msi" signed=$(find msi-signed -iname 'unim-*-x64.msi' -print -quit) if [[ -z "$signed" ]]; then echo "::warning::SignPath 서명 요청은 성공으로 보고됐으나 msi-signed/ 에서 MSI 를 찾지 못했습니다 — 미서명본을 그대로 사용합니다." echo "signed=false" >> "$GITHUB_OUTPUT" exit 0 fi cp "$signed" "$msi" echo "signed=true" >> "$GITHUB_OUTPUT" echo "Signed MSI installed: $msi ($(stat -c%s "$msi") bytes)" # ── Defender 오탐 게이트 (scripts/ci/verify-msi.ps1 -Phase scan) ─────── # # 2026-09-03 회사컴에서 Windows Defender 가 unim_tsf.dll(0.4.1) 을 # Trojan:Win32/Bearfoos.B!ml 로 오판해 하루 4회 격리한 사고 대응. 실측상 # 후킹·인젝션 API 없음·엔트로피 정상 — 원인은 무서명 + VERSIONINFO 공란 + # low prevalence 다. 서명·메타데이터 정비 전까지 재발 가능성이 있으므로 # MSI 와 원시 빌드 산출물(DLL/EXE)을 매 빌드 Defender 로 능동 스캔해 # 조기 발견한다. 아래 install/typing/uninstall 단계보다 먼저 돌아 # 실시간 검사 제외 경로 등록(verify-msi.ps1 Invoke-InstallPhase)이 판정을 # 가리기 전에 확인한다. # # 이 게이트가 이번 diff 로 처음 도입돼 실제 CI 러너에서 아직 그린 실적이 # 없다 — continue-on-error 로 승격 대기시키고, 그린 실적이 쌓이면 # 필수 게이트로 승격한다(Install/Uninstall 검증과 동일한 승격 방침). - name: 'Defender scan gate (scripts/ci/verify-msi.ps1 -Phase scan) (승격 대기)' continue-on-error: true timeout-minutes: 10 shell: powershell run: | $msi = "dist\unim-${{ steps.version.outputs.version }}-x64.msi" $ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" # 2026-09-03 2차 실측: -ScanPaths 두 값을 콤마로 이었더니(공백 없이 # "a","b") verify-msi.ps1 이 'target\...release,target\...release' 한 # 덩어리(콤마 포함 리터럴)로 받아 '경로 없음'으로 SKIP 됐다 — $ps 호출은 # 네이티브 프로세스 호출(argument mode)이라 인접 토큰의 콤마는 배열 # 연산자가 아니라 문자 그대로 이어붙는다(cmdlet 파라미터 바인딩과 다른 # 규칙). -File 로 넘겨받는 verify-msi.ps1 은 [string[]] 파라미터를 # 공백으로 구분된 별개 위치 인자로 정상 바인딩하므로, 콤마 대신 공백으로 # 나눠 진짜 2-원소 배열이 되게 한다. 산출 경로 자체는 위 cargo build # 단계(--target-dir 미지정, WIN_TARGET/i686 그대로)와 일치 — 경로 # 불일치가 아니라 이 인자 전달 버그였다. & $ps -NoProfile -ExecutionPolicy Bypass -File "scripts\ci\verify-msi.ps1" ` -MsiPath $msi -Phase scan -ArtifactDir "msi-verify" ` -ScanPaths "target\${env:WIN_TARGET}\release;target\i686-pc-windows-msvc\release" exit $LASTEXITCODE # ── 설치·등록·DLL 로드·기능·제거 실측 (scripts/ci/verify-msi.ps1) ────── # # 위 sanity check 는 "파일이 있고 100KB 를 넘는다" 까지다. 아래 3단계가 # 실제로 설치되는지 / TSF 가 등록되는지 / DLL 이 로드되는지 / 한글이 # 입력되는지 / 제거가 깨끗한지를 러너에서 직접 확인한다. # # 실행 호스트를 명시적으로 System32(64-bit) Windows PowerShell 5.1 로 # 고정한다 — (1) 레지스트리 64-bit 뷰가 필요하고, (2) 기본 STA 아파트먼트라 # Get-Clipboard·UIAutomation 이 동작하며, (3) ExecutionPolicy Bypass 를 # 스텝 설정에 의존하지 않고 스스로 건다. # 승격 대기: 2회 연속 통과 후 필수화(continue-on-error 제거). # 이 스텝들이 이번 diff 로 처음 실제 windows-2022 호스티드 러너에서 # 도는 것이라 verify-msi.ps1 자체의 미검증 실패(예: msiexec 대기 로직)가 # 뒤따르는 Uninstall/아티팩트 업로드/릴리스 첨부까지 통째로 막지 않도록 # 한시적으로 실패를 허용한다. 스크린샷·로그는 아래 'Upload verification # artifacts' 로 항상 올라간다. - name: 'Install + verify (scripts/ci/verify-msi.ps1) (승격 대기: 2회 연속 통과 후 필수화)' continue-on-error: true timeout-minutes: 10 shell: powershell run: | $msi = "dist\unim-${{ steps.version.outputs.version }}-x64.msi" $ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" & $ps -NoProfile -ExecutionPolicy Bypass -File "scripts\ci\verify-msi.ps1" ` -MsiPath $msi -Phase install -ArtifactDir "msi-verify" exit $LASTEXITCODE # 승격 대기: 2회 연속 통과 후 필수화. # 호스티드 러너의 데스크톱 대화형 여부(ctfmon/TSF 활성 세션)가 문서로 # 보장돼 있지 않아 첫 도입은 실패를 허용한다. 스크린샷·타이핑 로그는 # 아래 'Upload verification artifacts' 로 항상 올라간다. - name: 'Functional typing check (승격 대기: 2회 연속 통과 후 필수화)' continue-on-error: true timeout-minutes: 8 shell: powershell run: | $msi = "dist\unim-${{ steps.version.outputs.version }}-x64.msi" $ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" & $ps -NoProfile -ExecutionPolicy Bypass -File "scripts\ci\verify-msi.ps1" ` -MsiPath $msi -Phase typing -ArtifactDir "msi-verify" exit $LASTEXITCODE # 제거는 필수 게이트가 되어야 하지만(설치된 상태를 러너에 남기지 않고 # ForceDeleteOnUninstall 계약도 확인), Install 이 continue-on-error 로 # 승격 대기인 동안엔 이 스텝도 함께 승격 대기로 둔다 — 그래야 Install # 실패 시에도 always() 로 반드시 돌아 제거를 시도하고, 뒤따르는 아티팩트 # 업로드·릴리스 첨부가 스킵되지 않는다. - name: 'Uninstall + verify (scripts/ci/verify-msi.ps1) (승격 대기: 2회 연속 통과 후 필수화)' if: always() continue-on-error: true timeout-minutes: 8 shell: powershell run: | $msi = "dist\unim-${{ steps.version.outputs.version }}-x64.msi" $ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" & $ps -NoProfile -ExecutionPolicy Bypass -File "scripts\ci\verify-msi.ps1" ` -MsiPath $msi -Phase uninstall -ArtifactDir "msi-verify" exit $LASTEXITCODE - name: Upload verification artifacts if: always() uses: actions/upload-artifact@v4 with: name: unim-${{ steps.version.outputs.version }}-msi-verification path: msi-verify/** # 설치 자체가 시작조차 못 한 경우엔 비어 있을 수 있다 — 경고만. if-no-files-found: warn retention-days: 14 - name: Generate SHA256SUMS-msi shell: bash run: | set -euo pipefail # dist 로 진입 후 생성 → 매니페스트에 파일명만 남고 경로구분자 없음 # (install.ps1·'sha256sum -c' 가 flat tmpdir 에서 그대로 대조. linux-deb.yml # SHA256SUMS 패턴 대칭). Git Bash 텍스트 모드의 CR 변환을 원천 차단하려고 # -b(바이너리 마커). 태그 조건 없이 매 빌드 실행 → 브랜치 push 로그로 사전 실증. ( cd dist && sha256sum -b unim-*-x64.msi > SHA256SUMS-msi ) echo "── SHA256SUMS-msi ──" cat dist/SHA256SUMS-msi # 이름에 '-signed' 접미사가 붙으면 SignPath 서명본, 없으면 미서명본이다 # (steps.signed-msi 가 스킵/실패면 outputs.signed 는 빈 문자열). - name: Upload MSI artifact uses: actions/upload-artifact@v4 with: name: unim-${{ steps.version.outputs.version }}-x64-msi${{ steps.signed-msi.outputs.signed == 'true' && '-signed' || '' }} path: | dist/unim-${{ steps.version.outputs.version }}-x64.msi dist/SHA256SUMS-msi if-no-files-found: error retention-days: 30 - name: Upload raw binaries (for diagnostics) uses: actions/upload-artifact@v4 with: name: unim-${{ steps.version.outputs.version }}-x64-binaries path: | target/${{ env.WIN_TARGET }}/release/unim_tsf.dll target/${{ env.WIN_TARGET }}/release/unim_imm32.ime target/i686-pc-windows-msvc/release/unim_imm32.ime if-no-files-found: error retention-days: 7 # ── 릴리스 첨부 (태그 빌드 전용) ────────────────────────────────────── # 아티팩트는 로그인해야 받을 수 있고 보관 기간도 짧다. 일반 사용자가 받을 수 # 있도록 태그 빌드에서는 MSI 를 릴리스 페이지에 올린다. - name: Verify tag matches workspace version if: startsWith(github.ref, 'refs/tags/v') shell: bash run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" VER="${{ steps.version.outputs.version }}" if [[ "${TAG#v}" != "$VER" ]]; then echo "::error::태그($TAG) 와 워크스페이스 버전($VER) 불일치." exit 1 fi echo "Release tag $TAG matches version $VER" - name: Attach MSI to GitHub Release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} # body / name / make_latest 미지정 — 릴리스 본문·이름의 단일 작성자는 # linux-deb.yml 이다. 여기서 지정하면 먼저 만들어진 본문을 덮어쓴다. # 자산은 파일명 단위로 덧붙는다. files: | dist/unim-${{ steps.version.outputs.version }}-x64.msi dist/SHA256SUMS-msi