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

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

更新履歴(6件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
「完成形ワークフロー」が署名鍵を承認者付きのEnvironmentで守っていませんでした。5.5節で「署名用シークレットは承認者付きのEnvironmentに置く」と書いているのに、そのまま置けば動くと案内している本体はリポジトリシークレットを直接参照しており、書き込み権限を持つ人はワークフローを書き換えてタグを1本pushするだけで鍵を取り出せます。ジョブに`environment: release-signing`を付けました。あわせて、署名対象を`publish`配下の全EXE/DLLにしていたのを自社ビルドだけに絞りました。依存パッケージまで署名すると、ベンダーの署名を自社証明書で置き換え(`/as`なしのsignは既存署名を置換します)、第三者の未署名DLLを自社の成果物として配ることになります。
署名のワークフローが実行ファイル1つしか署名しておらず、同梱するDLLが未署名のまま配布物に入る書き方になっていたので、配布物に入る実行ファイルとDLLをまとめて署名する形に直しました。
クラウド署名サービスが日本の法人では利用できない点を独立した節にし、状況別の代替案を表に整理しました。あわせて対象読者と前提、PFX方式の完成形ワークフロー、GitHubリリースへの添付手段、パイプラインの全体図を追加しています。
本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21590042)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21590042 https://staging.comcomponent.com/blog/winforms-wpf-cicd-github-actions/

DOI(最新版)
10.5281/zenodo.21590042
DOI(この版)
10.5281/zenodo.21733017

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

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

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

対象読者と前提

自分の状況に当てはめながら読めるよう、この記事が前提にしている条件を先に挙げておきます。

  • 対象: WinForms / WPFで書かれたWindowsデスクトップアプリを、手元のVisual Studioでビルドして配っている開発者・チーム。CI/CDの経験は問いません。
  • リポジトリ: ソースがGitHubのリポジトリに入っていること(公開・非公開は問いません)。パブリックリポジトリでは標準ランナーが無料で、プライベートリポジトリでは実行時間が分単位の課金対象になります。1
  • プロジェクト形式: 本文のYAMLは.NET SDK形式のcsproj(net8.0-windowsなど)を前提にしています。.NET Framework 4.xの旧形式csprojでも考え方は同じで、dotnet buildの代わりにMSBuildとNuGet CLIを使います(第3章)。
  • 署名: 第5章は「これから証明書をどう用意するか」から扱います。すでにPFXファイルや社内CAがあるか、これから公的証明書を取るかで結論が変わるので、現状を確認しながら読んでください。
  • ゴール: 全自動配布ではなく、まず「誰のPCでもビルド・テストが再現でき、成果物が取り出せる」状態(第3章)です。そこから署名・配布へ段階的に足していきます。

パイプライン全体の形は次のようになります。どこまでを自動化の範囲にするかは第2章で扱います。

[日常] main への push / プルリクエスト
    └→ checkout → setup-dotnet → build → test → publish → upload-artifact (第3章)

[リリース] v1.2.3 タグの push
    └→ checkout → setup-dotnet → test
         → タグからバージョンを注入して publish (第4章)
         → 署名(signtool / クラウド署名サービス)(第5章)
         → 配布物を作る(zip / MSI / MSIX / ClickOnce)(第6章)
         → GitHub リリースへ添付(長期保管)(第4章)

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連携が容易なAzure Artifact Signing(旧Trusted Signing)は日本が対象地域に入っていないため、日本の開発者の現実解はCAのクラウドHSMオプションか、署名工程だけを手元に残す構成になります(5.2節)。5
  • 配布形式でCIへの乗せやすさが大きく違います。xcopy(zip)が最も簡単、MSIXは署名必須6、MSIはWiX等のCLI連携、ClickOnceはmsbuild /target:publishが必要で癖が強い、という順です。7
  • UIの自動テストをCIの必須関門にしないこと。ユニットテストはCI必須、UIテストはスモークに絞って別ジョブで回すのが現実解です。

この記事の知識マップ

WinForms/WPFデスクトップアプリのCI/CDは、まずwindows-latestランナーでのビルド+テストの自動化だけを入れることが実務の出発点で、次にv1.2.3のようなタグのpushでバージョンを注入するタグ駆動リリースを組み、成果物をGitHubリリースへ長期保管する。最大の壁はコード署名の自動化で、2023年6月以降は公的なOV証明書の秘密鍵がHSM保管必須になったため、日本では対象外のAzure Artifact Signingに頼れず、CAのクラウドHSMオプションか手元での署名工程が現実解になる。署名用シークレットは承認者付きのGitHub Actions Environmentに置かないと、書き込み権限を持つ人がワークフローを書き換えるだけで鍵を取り出せてしまう。配布形式ではxcopyが最も組み込みやすく、MSIXは署名が必須、ClickOnceはコマンドラインでの発行に癖がある。

WinForms/WPFアプリのCI/CDパイプラインの知識マップGitHub Actionsを使ったWinForms・WPFデスクトップアプリのビルド・タグ駆動バージョン採番・コード署名・配布形式ごとの自動化がどう連鎖し、署名鍵の管理がどのリスクと結び付くかを示す図。利用する前提とする前提とする前提とする利用するより先に行うべき利用する利用する前提とする前提とする両立しない推奨される対応軽減する原因になり得るで構成できる利用する利用する前提とする推奨される対応推奨される対応利用するデスクトップアプリのCI/CDパイプラインGitHub ActionsWindowsランナー(windows-latest)Windows FormsWPFビルド+テストの自動化コード署名証明書タグ駆動リリース(バージョン採番)GitHubリリースHSM/トークンでの秘密鍵保管Azure Artifact SigningGitHub Actions Environment(承認者付き環境)署名鍵の窃取リスクGitHub ActionsのシークレットSignToolコード署名のタイムスタンプMSIXxcopy配布(zip)ClickOnceMSI(Windows Installer)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

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

このYAMLを.github/workflows/build-and-test.ymlとして置けば、そのままpushで動きます。ただしsrc/MyApp/MyApp.csprojの部分は自分のリポジトリのプロジェクトパスに置き換えてください。以降のYAMLでも同じパスを例として使っています。リポジトリ直下にソリューションと1つのcsprojだけがある構成なら、dotnet publish -c Release -o 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のアーティファクトは一時的な受け渡しと割り切ります。

GitHubリリースへの添付は、専用アクションを使う方法と、ランナーに最初から入っているGitHub CLI(gh)を使う方法があります。8 どちらもジョブにcontents: writeの権限が要ります。

jobs:
  release:
    runs-on: windows-latest
    permissions:
      contents: write        # リリースの作成・アセット添付に必要
    steps:
      # ...(ビルドと publish は前掲のとおり)

      - name: Zip
        shell: pwsh
        run: Compress-Archive -Path publish\* -DestinationPath MyApp-${{ github.ref_name }}.zip

      # 方法A: 専用アクションを使う
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v3
        with:
          files: MyApp-${{ github.ref_name }}.zip

      # 方法B: ランナー同梱の GitHub CLI を使う(上のどちらか一方でよい)
      - name: Create GitHub Release (gh)
        shell: pwsh
        run: gh release create ${{ github.ref_name }} MyApp-${{ github.ref_name }}.zip --generate-notes
        env:
          GH_TOKEN: ${{ github.token }}

ghはGitHubホステッドランナーにプリインストールされていますが、ステップごとにGH_TOKEN環境変数へ必要なスコープを持つトークンを渡す必要があります。8

利点は運用が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が推奨です。9

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

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

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かトークンを挿したセルフホステッドランナーに残す折衷構成になります。

日本の開発者にとっての本題 ── Azure Artifact Signingは使えるのか

表の1行目に「利用可能な国・地域に制限あり」と書きましたが、日本の読者にとってはここが最重要の判断材料なので、独立して整理します。

Microsoftのドキュメントは、Azure Artifact Signing(旧Trusted Signing)が利用できるのは、法人は米国・カナダ・EU・英国、個人開発者は米国・カナダに限られると明記しています。5 つまり日本法人・日本在住の個人開発者は、現時点では対象外です。「クラウド署名が本命」という一般論をそのまま持ち込むと、アカウント作成の段階で止まります。この制限を前提にした選択肢は次のとおりです。

状況 現実的な選択
これから公的証明書を取る(日本法人) OV証明書+CAのクラウドHSMオプション。2023年6月以降、OV証明書の秘密鍵はHSMまたはハードウェアトークンでの保管が必須ですが、多くのCAはUSBトークンに加えてクラウドHSMの選択肢を用意しており、そちらならCIから署名を呼び出せます。5 CIに載せる予定があるなら、証明書を選ぶ段階でクラウドHSM対応の有無をCAに確認してください。トークンを買ってからでは変えられません
すでにUSBトークンで運用している 署名工程だけを、トークンを挿した手元PCまたはセルフホステッドランナーに残す。ビルド・テスト・バージョン採番までをGitHubホステッドランナーで自動化し、最後の署名だけ人の手を残す構成です
Microsoft Store(MSIX)で配布できる Store側でMicrosoftが再署名するため、自前の証明書が不要になります。5 配布方式を選び直せるなら、署名の悩みごと消える最短ルートです(ただしMSI/EXEインストーラーでStoreに出す場合は、publisher側の署名が必要です)
オープンソースプロジェクト SignPath Foundationが、条件を満たすOSSプロジェクトに無償のコード署名を提供しています。5
社内配布のみ 社内CAで発行した証明書と既存PFX方式で足ります(5.3節)。証明書をグループポリシーやIntuneで信頼済みルートとして配れる環境なら、公的証明書は不要です

Azure Artifact Signingの対象地域は将来広がる可能性があるので、CI/CDの署名設計を固めるときはその時点の対象地域を一次情報で確認してください。5

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

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

  • PFXはBase64文字列にしてGitHubのシークレットへ格納し、ジョブ内でファイルに復元します。バイナリをシークレットで扱う方法としてGitHub Docsが案内している手順です。10
  • パスワードは別のシークレットにします。シークレットの値はログ上で自動的にマスクされますが10、加工した派生値までは守られません。署名ステップ以外に環境変数を渡さないようにします。
  • フォークからのプルリクエストには(GITHUB_TOKENを除き)シークレットが渡りません。10 ただしジョブ自体は空のシークレットで実行されるため、上記の復元ステップは空文字列の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 }}

5.4 PFX方式の完成形ワークフロー

ここまでの断片(タグ駆動のバージョン注入・PFXの復元・signtoolの実行・成果物の発行)を1本につないだ形を示します。社内CAや既存PFXを持っている前提の構成で、これをそのままリポジトリの.github/workflows/release.ymlに置けば動きます。プロジェクトパスとタイムスタンプサーバーのURLは自分の環境に置き換えてください。

name: release

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

jobs:
  release:
    runs-on: windows-latest
    # 署名鍵に触れるジョブは、承認者付きの Environment に紐づける(5.5節)。
    # ここを省いてリポジトリシークレットのまま置くと、書き込み権限を持つ人
    # (または乗っ取られたアカウント)がワークフローを書き換えてタグを1本
    # push するだけで、署名鍵を取り出せます。この environment を指定し、
    # SIGNING_PFX_BASE64 と SIGNING_PFX_PASSWORD は Environment 側に置きます
    environment: release-signing
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

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

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

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

      # --- ここから署名 ---
      - 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 }}

      - name: Sign
        shell: pwsh
        run: |
          # signtool は Windows SDK 同梱。パスを固定で書かずに解決する
          $signtool = Get-ChildItem `
            "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
            Sort-Object FullName | Select-Object -Last 1

          # exe だけでなく、配布物に入る自社ビルドのDLLも署名する。
          # 配布先が App Control / AppLocker の発行元ルールでDLLも見る場合、
          # 署名のないDLLが1つでもあるとそこで止まる。
          #
          # ただし対象は自社ビルドだけに絞る。publish には NuGet 由来や
          # フレームワークのDLLも入るので、ワイルドカードで全部拾うと、
          # ベンダーが署名済みのDLLに自社証明書を上書きし(/as を付けない
          # sign は既存の署名を置き換える)、第三者の未署名DLLも
          # 「自社の成果物」として世に出す。発行元ベースの許可ルールや
          # 来歴の確認が壊れるので、名前で明示的に列挙する
          $ownAssemblies = @('MyApp', 'MyApp.Core', 'MyApp.Plugins')
          $targets = Get-ChildItem publish -Recurse -Include *.exe, *.dll |
            Where-Object { $ownAssemblies -contains $_.BaseName } |
            Select-Object -ExpandProperty FullName
          if (-not $targets) { throw '署名対象が見つかりません。publish の中身を確認してください。' }

          & $signtool.FullName sign `
            /f "$env:RUNNER_TEMP\sign.pfx" /p $env:PFX_PASSWORD `
            /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
            $targets
          if ($LASTEXITCODE -ne 0) { throw "署名に失敗しました (exit $LASTEXITCODE)" }
        env:
          PFX_PASSWORD: ${{ secrets.SIGNING_PFX_PASSWORD }}

      - name: Remove certificate
        if: always()
        shell: pwsh
        run: Remove-Item "$env:RUNNER_TEMP\sign.pfx" -ErrorAction SilentlyContinue

      # --- ここから成果物 ---
      - name: Zip
        shell: pwsh
        run: Compress-Archive -Path publish\* -DestinationPath MyApp-${{ github.ref_name }}.zip

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v3
        with:
          files: MyApp-${{ github.ref_name }}.zip

読むときの要点は5つです。

  • environment: release-signing を外さない。署名鍵をリポジトリシークレットに置いたままだと、リポジトリの書き込み権限を持つ人は誰でも、ワークフローを書き換えてタグを1本 push するだけで鍵を取り出せます。承認者付きの Environment にシークレットを置き、このジョブだけがそれを参照する形にします(5.5節)。この行が無いワークフローを「完成形」として運用しないでください。
  • 署名対象は自社ビルドだけに絞る。publish には依存パッケージのDLLも入ります。まとめて署名すると、ベンダーの署名を自社証明書で置き換えたうえ、第三者の未署名DLLを自社の成果物として配ることになります。どうしても第三者のバイナリに署名を足す必要があるなら、置き換えではなく /as で追加します。
  • 署名はdotnet publishの後、圧縮の前。zipに固めてから署名しても、中のEXEは署名されません。MSIやMSIXを作る場合も、まず中身のEXE/DLLに署名し、それからパッケージを作り、最後にパッケージ自体へ署名する順になります。
  • PFXは使い終わったら消す。if: always()を付けて、署名が失敗しても削除ステップが走るようにします。GitHubホステッドランナーはジョブごとに破棄されるので1必須ではありませんが、セルフホステッドランナーへ移したときに事故になります。
  • このワークフローはタグのpushでしか走らない。フォークからのプルリクエストでは発火しないため、5.3節で触れたシークレット空問題を構造的に避けられます。

クラウド署名サービスやクラウドHSMを使う場合は、「Restore signing certificate」と「Sign」の2ステップが、サービス側が提供するアクションまたはCLI呼び出しに置き換わるだけで、前後の形は変わりません。

5.5 署名鍵に触れるワークフローを絞る

シークレットの扱い方(5.3節)と並ぶもう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保管必須となった現在、Azure Artifact Signingは日本が対象地域外なので、CAのクラウドHSMオプションを証明書の選定段階で確認するのが実務の入口です。PFX+シークレット方式は社内CA・既存証明書向けで、5.4節に完成形のワークフローを載せました。510
  • タグビルドの成果物はsoftprops/action-gh-releaseかランナー同梱のgh release createでGitHubリリースへ添付し、長期保管します(ジョブにcontents: writeが必要)。8
  • 配布形式の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 4 5

  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 8 9 10 11 12

  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. GitHub Docs, Using GitHub CLI in workflows. GitHub CLI(gh)がGitHubホステッドランナーすべてにプリインストールされていること、ghを使う各ステップで必要なスコープを持つトークンをGH_TOKEN環境変数に設定する必要があることについて。  2 3

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

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

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

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

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

よくある質問

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

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

ブログ一覧に戻る