WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する

· · CI/CD, GitHub Actions, WinForms, WPF, C#, .NET, コード署名, MSIX, Deployment, Windows開発, 判断表

「リリースは、あの人のPCじゃないとビルドできないんです」──WinFormsやWPFの業務アプリの相談で、本当によく聞く言葉です。手元のVisual StudioでReleaseビルドして、zipに固めて共有フォルダーに置く。動いてはいるものの、その開発者が休んだ日にバグ修正版を出せるのか、誰も答えられません。

WebアプリのCI/CDは情報があふれているのに、デスクトップアプリとなると途端に薄くなります。「デプロイ先がサーバーではなく客先のPC」という根本的な違いがあるので、Webの記事をそのまま真似できないのも確かです。しかし、ビルドとテストの自動化までなら、デスクトップアプリでもWebとほぼ同じ手間で組めます。壁になるのはその先の署名と配布であり、そこは配布方式ごとに現実解が違います。

当ブログでは「Windowsアプリの配布方式の判断表」で配布方式の選び方を、「SmartScreenとコード署名」で署名の考え方を整理しました。この記事はその2本を前提に、GitHub Actionsを使ってWinForms / WPFアプリのビルド・テスト・バージョン採番・署名・配布物作成をどこまで自動化するかを実務目線でまとめます。

1. まず結論

  • 最大のリスクは「開発者のPCでしかビルドできない」状態です。CI/CDの第一目標は配布の全自動化ではなく、誰のPCにも依存せずビルドが再現できることです。
  • 最小構成はビルド+テストの自動化だけで十分に価値があります。windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact、YAML 1ファイルで組めます。12
  • WinForms / WPFはWindowsランナーが前提です。net8.0-windowsのようなWindows専用TFMを対象とするため3、テスト実行までCIで行うならWindows環境が必要です。
  • バージョン採番はタグ駆動が落としどころです。v1.2.3タグのpushでリリースビルドを起動し、MSBuildのVersionプロパティにタグの値を注入します。4
  • 署名が自動化の最大の壁です。2023年6月以降、公的なOV証明書の秘密鍵はHSM保管が必須になり、「PFXをシークレットに置いてsigntool」という従来の定番はそのままでは使えません。CIで完結させるならクラウド署名サービス経由が現実的です。5
  • 配布形式でCIへの乗せやすさが大きく違います。xcopy(zip)が最も簡単、MSIXは署名必須6、MSIはWiX等のCLI連携、ClickOnceはmsbuild /target:publishが必要で癖が強い、という順です。7
  • UIの自動テストをCIの必須関門にしないこと。ユニットテストはCI必須、UIテストはスモークに絞って別ジョブで回すのが現実解です。

2. デスクトップアプリのCI/CDはWebと何が違うか

WebアプリのCI/CDの型(push→ビルド→テスト→サーバーへデプロイ)をそのまま持ち込めない理由を、先に整理します。

観点 Webアプリ WinForms / WPFデスクトップアプリ
デプロイ先 自分たちが管理するサーバー 客先・現場のPC(管理外)
配布の単位 サーバー上で一斉切り替え MSI / MSIX / ClickOnce / zipなど多様。展開のタイミングは相手次第
ロールバック サーバー側で戻せる 配布済みPCからは容易に戻せない。旧版インストーラーの保管が必須
署名 通常不要(TLSはインフラ側) 実行ファイル・パッケージへのコード署名が実質必須
ビルド環境 Linuxランナーで完結しやすい Windowsランナーが前提
テスト ヘッドレスで完結しやすい ユニットテストは同じ。UIテストはデスクトップセッションが必要
「デプロイ」の意味 本番反映まで CIの守備範囲は「配布物の完成」まで。インストールは別工程

重要なのは最後の行です。デスクトップアプリでは、CI/CDパイプラインの出口は「本番反映」ではなく「署名済みの配布物が、いつでも取り出せる場所に置かれていること」です。そこから先(客先への展開、自動更新)は配布方式の設計の話で、「配布方式の判断表」で扱った領域になります。逆に言えば、出口をそう割り切れば、デスクトップアプリのCI/CDはWebと同じ道具立てで組めます。

3. 最小構成 ── GitHub Actionsでビルド+テスト

最初に入れるべきはこれだけです。GitHubホステッドランナーはジョブごとに新しいVMが割り当てられるため1、pushのたびにまっさらなWindows上でビルドとテストが走り、「あの人のPCにしか入っていないSDK」への依存がその場で発覚します。

name: build-and-test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: windows-latest   # WinForms / WPF は Windows ランナー必須
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build

      - name: Publish
        run: dotnet publish src/MyApp/MyApp.csproj -c Release -o publish

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp
          path: publish

ポイントを3つ補足します。

第一に、runs-on: windows-latestが基本です。WinForms / WPFプロジェクトはTargetFrameworknet8.0-windowsのようなWindows専用TFMで、UseWindowsFormsまたはUseWPFを有効にした.NETデスクトップSDKのプロジェクトです。3 厳密にはコンパイルだけならLinuxランナーでもEnableWindowsTargetingを有効にすればビルドできますが、dotnet testなど実行を伴うステップにはWindows環境が要るため、テストまで1ジョブで回すこの構成では素直にWindowsランナーを使います。.NET Framework 4.x(旧形式csproj)の場合はdotnet buildではなくMSBuildとNuGet CLIを使いますが、どちらもWindowsランナーにプリインストールされており、考え方は同じです。

第二に、actions/upload-artifactで成果物を必ず残します。「そのビルドの成果物一式がGitHubから取り出せる」ことが、脱・属人PCの実体です。急ぎの動作確認版もActionsの画面からzipを落とすだけになります。

第三に、この段階では署名も配布もまだやりません。この最小構成だけで「mainが常にビルド・テスト可能」「誰でも同じ成果物を取り出せる」という2つの保証が手に入り、筆者の経験では小規模チームの悩みの大半はこれで解消します。

4. バージョン番号の自動採番 ── タグ駆動リリース

次の段階は「このzip、バージョンいくつ?」問題の解消です。手元ビルドの運用では、csprojのVersionを書き換え忘れて同じ1.0.0が何世代も存在する事故が定番です。実務の落としどころはタグ駆動リリースで、リリースしたいコミットにv1.2.3のようなタグを打つと、それをトリガーにワークフローが走り、タグ名から取ったバージョンをビルドに注入します。

name: release

on:
  push:
    tags: [ 'v*' ]

jobs:
  release:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      # タグのpushでは3章のビルド+テストのワークフローは発火しないため、
      # リリースの成果物を作る前にここでもテストを通す
      - name: Test
        run: dotnet test --configuration Release

      - name: Publish with version from tag
        shell: pwsh
        run: |
          $version = $env:GITHUB_REF_NAME.TrimStart('v')   # v1.2.3 -> 1.2.3
          dotnet publish src/MyApp/MyApp.csproj `
            -c Release -o publish `
            -p:Version=$version

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-${{ github.ref_name }}
          path: publish

-p:Version=1.2.3のようにMSBuildプロパティとして渡すと、.NET SDKプロジェクトではAssemblyVersionFileVersionVersionのプレフィックス(サフィックスを除いた部分)から、InformationalVersionVersionそのものから既定で生成されます。4 csprojには開発用の仮値だけを置き、リリース時の正式なバージョンはタグだけが持つ一元管理になります。

1つ注意があります。actions/upload-artifactの成果物にはリポジトリの保持期間(既定90日)があり、期限が切れると消えます。デスクトップアプリでは切り戻し用に旧バージョンのインストーラーを長期保管する必要があるため、タグビルドの成果物はGitHubリリースに添付するなどの恒久的な置き場所へ発行し、Actionsのアーティファクトは一時的な受け渡しと割り切ります。

利点は運用がGitに閉じることです。「お客様環境の1.2.3はどのコミットか」はタグで確定し、EXEのプロパティに出るファイルバージョンとGitのタグが機械的に一致します。さらにInformationalVersionには.NET 8 SDK以降、Gitのコミットハッシュ(SourceRevisionId)が既定で付加されるため4、バージョン表示画面にこれを出しておけば成果物から直接コミットを特定できます。

5. コード署名をCIに組み込む ── ここが最大の壁

ビルドとバージョンまでは順調に自動化できたチームが、ほぼ確実に立ち止まるのが署名です。Store外で配布するWindowsアプリにコード署名が実質必須である理由(SmartScreen、企業のセキュリティ製品、改ざん検知)は「SmartScreenとコード署名」で整理したので、ここではCIのどこで、どうやって実行するかに絞ります。

5.1 signtoolの基本形

署名の実行自体は1コマンドです。signtoolはWindows SDKに含まれ、GitHubのWindowsランナーでも利用できます。現行SDKでは/fd(ファイルダイジェスト)と/td(タイムスタンプダイジェスト)の指定が必須で、SHA256が推奨です。8

signtool sign /f MyCert.pfx /p $env:PFX_PASSWORD `
  /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
  publish\MyApp.exe

タイムスタンプ(/tr)は省略可能ですが、必ず付けます。タイムスタンプがあれば、証明書の期限が切れた後も「署名時点では有効だった」ことが検証でき、配布済みファイルの署名が生き続けます。86

5.2 証明書の種類とCI組み込みの現実

問題はコマンドではなく、秘密鍵をどこに置くかです。証明書の入手形態によって、CIへの組み込み方が根本的に変わります。

証明書の形態 秘密鍵の場所 CIへの組み込み 備考
クラウド署名サービス(Azure Artifact Signing = 旧Trusted Signing 等) クラウド側 組み込みやすい(本命)。GitHub Actions等との連携が前提の設計 利用可能な国・地域に制限あり(法人は米国・カナダ・EU・英国等)5
OV証明書(2023年6月以降の新規発行) HSM / USBトークン必須 トークンがランナーに挿せないためそのままでは不可。CAのクラウドHSMオプションなら可 CA/Browser Forum要件による5
EV証明書 HSM / USBトークン 同上 SmartScreenの即時信頼効果は2024年に廃止済み。署名運用上はOVと同列に考える5
旧来のPFXファイル(過去に発行されたもの・社内CA・自己署名) ファイル シークレットにBase64で格納して復元(下記) 公開配布用の新規取得ではこの形は原則もう入手できない

つまり、検索でよく出てくる「PFXをGitHubのシークレットに置いてsigntoolで署名」という構成は、社内CAや既存PFXでは今も有効ですが、これから公的証明書を取得するケースでは前提が崩れています。新規に組むなら、CI連携を最初からサポートするクラウド署名サービスを軸に検討するのが現実的です。5 USBトークンで運用中なら、署名工程だけ手元のPCかトークンを挿したセルフホステッドランナーに残す折衷構成になります。

5.3 シークレット管理の注意

PFX方式(社内CA・既存証明書)をCIに載せる場合の定石です。

  • PFXはBase64文字列にしてGitHubのシークレットへ格納し、ジョブ内でファイルに復元します。バイナリをシークレットで扱う方法としてGitHub Docsが案内している手順です。9
  • パスワードは別のシークレットにします。シークレットの値はログ上で自動的にマスクされますが9、加工した派生値までは守られません。署名ステップ以外に環境変数を渡さないようにします。
  • フォークからのプルリクエストには(GITHUB_TOKENを除き)シークレットが渡りません。9 ただしジョブ自体は空のシークレットで実行されるため、上記の復元ステップは空文字列のBase64デコードで失敗します。署名ステップは第4章のようなタグ起動のリリースワークフロー(フォークPRでは発火しない)に分離するか、if: github.event_name != 'pull_request' のような条件を付けて明示的にスキップさせます。
      - name: Restore signing certificate
        shell: pwsh
        run: |
          $bytes = [Convert]::FromBase64String($env:PFX_BASE64)
          [IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", $bytes)
        env:
          PFX_BASE64: ${{ secrets.SIGNING_PFX_BASE64 }}

もう1つの急所は、署名鍵にアクセスできるワークフローを絞ることです。リポジトリへの書き込み権限を持つ人はワークフローを書き換えられるため、署名用シークレットは承認者付きのEnvironmentに置いてリリースワークフローだけが参照できるようにします。署名済みバイナリは「自社が作った」ことの証明そのものなので、鍵の扱いは自動更新の配信基盤と同じレベルの信頼境界として設計します(この観点は「自動アップデートのセキュリティ」参照)。

6. 配布形式ごとのCI/CD統合の判断表

署名まで来たら、最後は配布物の形です。方式選定そのものは「配布方式の判断表」に譲り、ここではCI/CDの観点だけで比較します。

配布形式 CIでの作りやすさ CIでの作成手段 署名の要件 自動更新
xcopy(zip配布) 最も簡単 dotnet publish+圧縮のみ EXE/DLLへの署名(推奨) なし(手動展開)
xcopy+自作アップデータ 簡単(ビルドは)。更新配信の設計は別途重い dotnet publish+マニフェスト生成 EXE署名+更新ファイルの検証設計が必須 自作(信頼境界の設計が必要)
MSI 中程度 WiX等のツールをCLIで実行 MSIファイルへの署名(推奨〜実質必須) なし(別途配布の仕組みが必要)
MSIX 中程度 MSBuild / MakeAppx+signtool パッケージ署名が必須(未署名はインストール不可)6 App Installer等で対応可能
ClickOnce 癖が強い msbuild /target:publish+発行プロファイル(dotnet CLI非対応)7 マニフェスト署名+EXE署名 組み込み(方式の主目的)

補足します。

  • xcopy(zip): 第3章・第4章のワークフローがほぼそのまま完成形です。配布方式が最終的に何であれ、まず一度この形を通すのが近道です。
  • MSI: インストーラー定義(WiX等)をリポジトリに含め、CLIでビルドします。生成そのものより「MSIに何を含めるか(サービス登録、per-machine/per-user)」の設計が本体です。
  • MSIX: Windowsは署名されていないMSIXのインストールを許可しないため、署名の自動化とセットでないとCI化が完結しません6 逆に署名基盤が整っていればCIに乗せやすい形式です。Microsoft Store配布ならStore側で再署名されるため、自前の証明書が不要になる別解もあります。5
  • ClickOnce: dotnet CLIからは発行できず、発行プロファイル(.pubxml)を指定したmsbuild /target:publish /p:PublishProfile=...を使います。IDEでは発行のたびに自動加算されるリビジョン番号(ApplicationRevision)がコマンドラインでは加算されないため7、第4章のタグ駆動でバージョンを明示的に渡す設計が必須です。その際の注意として、ClickOnceの更新判定は-p:Version(アセンブリ情報)ではなくデプロイ側のバージョン(ApplicationVersion / ApplicationRevision)で行われるため、タグから作った4部形式の値を/p:ApplicationVersion=1.2.3.0のように別途渡さないと、新しいリリースが更新として認識されません。仕組みと向き不向きは「ClickOnceとは何か」で解説しています。

CI/CDの観点だけで言えば、「zipで始めて、配布要件が固まったらMSIXかMSIをジョブとして追加する」のが増分の少ない進め方です。前段(ビルド・テスト・バージョン)は全形式で共通なので、後から配布形式のステップを差し替えても資産は無駄になりません。

7. テストの自動化をどこまでやるか

最後に、CIの関門(必須チェック)としてどこまでテストを課すかの線引きです。

テストの層 CIでの扱い 理由
ユニットテスト(ロジック) 必須関門。プルリクエストごとに実行 速い・安定・Windowsランナーでそのまま動く
画面なし結合テスト(DB・ファイルI/O) 原則必須。遅ければ夜間実行に分離 外部依存の初期化に工夫が要るが自動化価値が高い
UI自動テスト(スモーク) 別ジョブで少数だけ。起動〜主要画面遷移程度 デスクトップセッション必須で不安定要因が多い
UI自動テスト(網羅) CIの関門にしない 維持コストが利益を上回りやすい

デスクトップアプリで自動化価値が最も高いのは、UIではなくその下です。コードビハインドに業務ロジックが埋まっているとユニットテストが書けないので、ロジックを画面から分離すること自体がCI/CDの前提投資になります。UI自動テストは「起動して、ログインして、主要画面が開く」スモークに絞り、夜間などの別トリガーで回すのが現実解です。ランナー上のUIテストには画面セッション・解像度・タイミングの罠が多く、この領域は「Windowsデスクトップアプリの UI 自動テスト」でCI・無人実行の落とし穴まで含めて扱っています。

8. まとめ

  • デスクトップアプリのCI/CDは、出口を「署名済み配布物の完成」と定義すれば、Webと同じ道具立てで組めます。
  • 最小構成はwindows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact。これだけで「開発者のPCでしかビルドできない」リスクが消えます。12
  • WinForms / WPFはnet8.0-windowsなどWindows専用TFMのため、Windowsランナーが前提です。3
  • バージョンはv1.2.3タグ→-p:Version注入のタグ駆動で一元化します。4
  • 署名はCI自動化の最大の壁です。OV証明書もHSM保管必須となった現在、CIで完結させるならクラウド署名サービスが本命で、PFX+シークレット方式は社内CA・既存証明書向けです。59
  • 配布形式のCIへの乗せやすさは、xcopy(zip)→MSI / MSIX→ClickOnceの順。MSIXは署名必須6、ClickOnceはmsbuild /target:publishとリビジョン非自動加算に注意が要ります。7
  • ユニットテストをCIの必須関門に、UIテストはスモークに絞って別ジョブで。ロジックの画面からの分離が前提投資です。

関連記事

関連する相談領域

合同会社小村ソフトでは、WinForms / WPFアプリの開発に加えて、手元ビルド運用からのCI/CD移行、GitHub Actionsによるビルド・署名・配布パイプラインの設計、既存デスクトップアプリのテスト可能化(ロジック分離)を扱っています。

参考リンク

  1. GitHub Docs, GitHub-hosted runners reference. windows-latest等のランナーラベル、ジョブごとに新しい仮想マシンが割り当てられること、パブリックリポジトリでは標準ランナーが無料であることについて。  2 3

  2. Microsoft Learn, GitHub Actions and .NET. GitHub Actionsによる.NETのCI/CD、actions/checkout・actions/setup-dotnetの役割、ワークフロー内でのdotnet restore / build / test / publishの利用について。  2

  3. Microsoft Learn, MSBuild reference for .NET Desktop SDK projects. WinForms / WPFプロジェクトはnet8.0-windowsのようなWindows固有のTFMを指定し、UseWindowsForms / UseWPFで.NETデスクトップSDKを有効化することについて。  2 3

  4. Microsoft Learn, Set assembly attributes in a project file. VersionプロパティからAssemblyVersion / FileVersion(サフィックス除き)とInformationalVersionが既定で生成されること、.NET 8 SDK以降はSourceRevisionId(コミットハッシュ)がInformationalVersionに付加されることについて。  2 3 4

  5. Microsoft Learn, Code signing options for Windows app developers. 2023年6月以降CA/Browser Forum要件でOV証明書の秘密鍵はHSM/ハードウェアトークン保管が必須であること、EV証明書の初回SmartScreen回避が2024年に廃止されたこと、Azure Artifact Signing(旧Trusted Signing)はトークン不要でGitHub Actions等と統合でき利用地域に制限があること、StoreのMSIX配布ではMicrosoftが再署名することについて。  2 3 4 5 6 7

  6. Microsoft Learn, Sign an MSIX package. WindowsはMSIXパッケージへの有効なコード署名を必須とすること、タイムスタンプにより証明書の期限後も署名検証が有効に保たれることについて。  2 3 4 5

  7. Microsoft Learn, Build .NET ClickOnce applications from the command line. .NETのClickOnce発行には発行プロファイルを指定したmsbuild /target:publishが必要であること、ApplicationRevisionがコマンドラインビルドでは自動加算されないことについて。  2 3 4

  8. Microsoft Learn, SignTool. SignToolはWindows SDKに含まれること、現行ビルドでは/fd/tdの指定が必須でSHA256が推奨であること、/trによるRFC 3161タイムスタンプ指定について。  2

  9. GitHub Docs, Using secrets in GitHub Actions. シークレット値のログからの自動秘匿、証明書等のバイナリをBase64でシークレットに格納しジョブ内で復元する手順、フォーク起動のワークフローにはシークレットが渡されないことについて。  2 3 4

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

自社開発のWindowsアプリがウイルス扱いされたら ── Microsoft Defenderの誤検知対応と、性能影響との付き合い方

配布した自社開発のWindowsアプリがMicrosoft Defenderにウイルスとして検知・隔離されたときの正規の対処を整理します。機械学習・クラウド保護・レピュテーションという現代のウイルス対策の仕組み、Microsoftへの誤検知報告(ファイル申請)、隔離からの復...

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

WinForms / WPFアプリのビルドはGitHub Actionsのどのランナーで動かすべきですか?
windows-latestなどのWindowsランナーを使います。WinForms / WPFプロジェクトはnet8.0-windowsのようなWindows専用のターゲットフレームワークを対象としており、ビルド成果物の動作確認やdotnet testによるテスト実行にはWindows環境が必要です。GitHubホステッドランナーはジョブごとに新しい仮想マシンが割り当てられるため、開発者のPCに依存しない再現可能なビルドが得られます。パブリックリポジトリでは標準ランナーは無料で、プライベートリポジトリでは分単位の課金対象になります。
コード署名はCIで完全に自動化できますか?
証明書の持ち方次第です。以前のようにPFXファイルをシークレットに置いて signtool で署名する方式は、2023年6月以降にCA/Browser Forumの要件で公的なOV証明書の秘密鍵がHSM(ハードウェア)保管必須になったため、新規取得の証明書では原則使えません。USBトークン型の証明書はクラウドのランナーに挿せないので、CIで完全自動化するならAzure Artifact Signing(旧Trusted Signing)のようなクラウド署名サービスか、CAが提供するクラウドHSMを経由するのが現実的です。トークン運用のままなら、署名工程だけ手元またはセルフホステッドランナーに残す構成になります。
まずどこから自動化を始めるべきですか?
ビルド+テストの自動化だけをまず入れるべきです。pushのたびにwindows-latestランナーでdotnet build / dotnet testが走る状態を作るだけで、「開発者のPCでしかビルドできない」「マージでビルドが壊れたことに配布直前まで気づかない」という最大のリスクが消えます。署名・インストーラー作成・配布の自動化はその後で段階的に足せばよく、最初から全部を組もうとすると署名周りで止まりがちです。
配布形式(MSI / MSIX / ClickOnce / xcopy)によってCI/CDの組みやすさは変わりますか?
大きく変わります。xcopy配布(zip)はdotnet publishの出力を圧縮するだけなので最も簡単です。MSIXはMSBuildとsigntoolでCIに乗せられますが、パッケージへの署名が必須です。MSIはWiXなどのツールをCIから呼ぶ形で自動化できます。ClickOnceはdotnet CLIでは発行できず、msbuild /target:publishと発行プロファイルの組み合わせが必要で、コマンドラインではリビジョン番号が自動加算されない点にも注意が要ります。配布方式を決める段階でCIへの乗せやすさも判断材料に入れるべきです。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る