しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

App Center と Azure Pipelines を利用した WPF (.NET Core) アプリケーション開発の効率化

App Center SDK 3.0.0 で WPF (.NET Core) に正式対応したのと、ずっと SR を投げていた Azure Pipelines の問題が解消されたので、この二つを使って WPF アプリケーションのビルドを自動化しました。

例によって対象の WPF アプリケーションは WinQuickLook です。すでにこのアプリケーションは .NET Core への移行を行っていて、Desktop Bridge を使って Windows Store へ公開しています。

WPF アプリケーションのモニタリングに対応したサービスは App Center ぐらいしか選択肢がないので、正式に対応されたのは非常に喜ばしいです。

.NET Core への移行に関する話は以前に書いた以下のエントリを参照してください。Tiered Compilation と ReadyToRun を有効にしていますが、Desktop Bridge との組み合わせは設定が少し複雑でした。

今回の目標は App Center を使ってアプリケーションのクラッシュログを収集することと、Azure Pipelines を使って署名付きの msixupload ファイルを作成して、簡単に Windows Store へのアップロードが行える状態にすることです。特に署名周りの自動化は必須でした。

リリース用ビルドは GitHub Release を利用して、タグが打たれたタイミングで自動でバージョン付きでビルドを行います。これぐらいやっておかないとリリースの手間が省けないです。

App Center を組み込む

WPF アプリケーションへの App Center の組み込みは、UWP などと同じなので迷うことはないはずです。ドキュメントも用意されているので WPF / Win Forms のどちらでも簡単に対応できます。

手順を要約すると、以下のパッケージをインストールした後に、OnStartup をオーバーライドして初期化コードを追加するだけです。バージョンは 3.0.0 以上をインストールします。

Analytics だけではなく Crashes も WPF がサポートされているので、以下のような初期化コードを追加すればクラッシュログも簡単に App Center で確認できるようになります。

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        AppCenter.Start("00000000-0000-0000-0000-000000000000", typeof(Analytics), typeof(Crashes));
    }
}

以前は Application Insights が提供されていましたが、App Center を使うようにした方が良いでしょう。

設定後、アプリケーションを立ち上げると App Center で各メトリックが確認できるようになります。

f:id:shiba-yan:20200303181942p:plain

ちゃんとクラッシュログも収集されるので、不具合の特定と修正が格段に行いやすくなりました。UWP では Partner Center からある程度見れましたが、WPF の場合は何のログも残らないので苦労してました。

f:id:shiba-yan:20200303182051p:plain

まだ標準で送信されるイベントは少ないので、必要な場所でカスタムイベントを送信するようにすれば良さそうです。個人的にはクラッシュログが見れる時点でかなり楽になりました。

Azure Pipelines で CI を組む

Windows Store へアプリケーションをアップロードする際には署名された msixupload ファイルが必要なので、当然ながら証明書を用意する必要があります。

Visual Studio だとあまり意識しなくても行えますが、署名周りはかなり属人化してしまうので自動化した方が良いです。UWP / MSIX に関しては Azure Pipelines を前提としたドキュメントが公開されてます。

ドキュメントを読んでも MSBuild に渡すパラメータが結構わかりにくいですが、以下のようなパラメータを渡せば署名付きの msixupload ファイルが作成されます。

ちなみに msixupload の作成は .NET Core CLI では行えないので、MSBuild を使う必要があります。

msbuild WpfApp.wapproj /p:Configuration=Release /p:UapAppxPackageBuildMode=StoreUpload \
    /p:AppxBundlePlatforms="x86|x64" /p:AppxPackageDir=".\packed" /p:AppxBundle=Always \
    /p:AppxPackageSigningEnabled=true /p:PackageCertificateThumbprint="" \
    /p:PackageCertificateKeyFile=WpfApp.pfx /p:PackageCertificatePassword=PFX_PASS

MSBuild を使って msixupload の作成が出来れば、後は Azure Pipelines の YAML 定義を書くだけです。

msixupload の作成には証明書とパスワードが必要なので、それぞれ Secure files と Variable groups を使って Azure Pipelines 上で実現します。

パスワードなどの機密情報を扱う場合には、そのまま Variable groups を使って機密情報のフラグを立てるシンプルな方法がありますが、今回は Key Vault を参照するようにしてみました。

Azure Subscription の追加と、参照する Key Vault の Access Policy の設定が必要ですが、Access Policy で許可していないユーザーやサービスプリンシパル以外はアクセス出来ないので安全です。

f:id:shiba-yan:20200101222007p:plain

Variable groups に Key Vault をリンクすると、どの設定を参照するかを設定できます。

f:id:shiba-yan:20200101222018p:plain

実行時のみ値を参照するようになっているので、安心して保存することが出来ますが、もちろん Key Vault 側の Access Policy がザルになっていては意味がないので注意します。

なんやかんやで、作成した YAML は以下のようになりました。msixupload のビルドには MSBuild が必要ですが、WPF (.NET Core) のビルドには最新の .NET Core が必要なので明示的にインストールしています。NuGet もデフォルトで入っているバージョンではエラーになったので最新を入れています。

trigger:
- master

variables:
- group: Secrets
- name: BuildConfiguration
  value: Release
- name: DotNetSdkVersion
  value: 3.1.x
- name: BundlePlatforms
  value: x86|x64

pool:
  vmImage: 'windows-latest'

steps:
- task: DownloadSecureFile@1
  name: signingCert
  inputs:
    secureFile: 'WpfApp.pfx'

- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '$(DotNetSdkVersion)'

- task: NuGetToolInstaller@1
  inputs:
    versionSpec: '5.x'

- task: NuGetCommand@2
  inputs:
    command: 'restore'
    restoreSolution: '**/*.sln'
    feedsToUse: 'select'
    verbosityRestore: 'Normal'

- task: MSBuild@1
  inputs:
    solution: '**/*.wapproj'
    configuration: 'Release'
    msbuildArguments: '/p:UapAppxPackageBuildMode=StoreUpload
                       /p:AppxBundlePlatforms="$(BundlePlatforms)"
                       /p:AppxPackageDir="$(Build.SourcesDirectory)/packed"
                       /p:AppxBundle=Always
                       /p:AppxPackageSigningEnabled=true
                       /p:PackageCertificateThumbprint=""
                       /p:PackageCertificateKeyFile="$(signingCert.secureFilePath)"
                       /p:PackageCertificatePassword="$(PfxPassword)"'

- publish: packed
  artifact: msix

この定義を使ってビルドを実行すると、Pipeline Artifacts に msixupload などのビルド結果が保存されます。

f:id:shiba-yan:20200305223043p:plain

Windows Store へのアップロードも以下のような拡張をインストールすれば、Azure Pipelines から実行できるはずですが Azure AD 周りの設定がめんどくさそうだったので試してはいません。

暇なときにでも試してみようかと思います。とりあえず拡張のインストールだけは行いました。

GitHub Release を使ってバージョンを自動的に付ける

最後は GitHub Release を使って、自動的にタグ名からバージョンを付けるようにしてみます。これまでもタグ名からバージョンを自動で付与する定義はたくさん書いてきましたが、Desktop Bridge を使っている場合には少し手間がかかります。

理由としては Desktop Bridge でパッケージ化したアプリケーションのバージョンと、アセンブリにメタデータとして付与されているバージョンは別ものだからです。

アセンブリにバージョンを付ける場合は MSBuild のパラメータに /p:Version=1.0.0 のように付ければ良いですが、パッケージ化したアプリケーションのバージョンは Package.appxmanifest で定義されています。なのでビルド中にバージョンを書き換えてあげる必要があります。

- powershell: 'echo "##vso[task.setvariable variable=ApplicationVersion]$($env:Build_SourceBranchName.Substring(1))"'
  displayName: 'Set ApplicationVersion'

- powershell: |
    [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
    $path = "WpfApp/Package.appxmanifest"
    $doc = [System.Xml.Linq.XDocument]::Load($path)
    $xName = [System.Xml.Linq.XName]"{http://schemas.microsoft.com/appx/manifest/foundation/windows10}Identity"
    $doc.Root.Element($xName).Attribute("Version").Value = "$(ApplicationVersion).0";
    $doc.Save($path)
  displayName: 'Update Package Manifest'

- task: MSBuild@1
  inputs:
    solution: '**/*.sln'
    configuration: $(BuildConfiguration)
    msbuildArguments: '/p:Version="$(ApplicationVersion)"
                       /p:UapAppxPackageBuildMode=StoreUpload
                       /p:AppxBundlePlatforms="$(BundlePlatforms)"
                       /p:AppxPackageDir="$(Build.SourcesDirectory)/packed"
                       /p:AppxBundle=Always
                       /p:AppxPackageSigningEnabled=true
                       /p:PackageCertificateThumbprint=""
                       /p:PackageCertificateKeyFile="$(signingCert.secureFilePath)"
                       /p:PackageCertificatePassword="$(PfxPassword)"
                       /verbosity:minimal'
  displayName: Build MSIX Package

複雑そうな PowerShell スクリプトを書いていますが、公式ドキュメントに書いてある内容をほぼそのまま持ってきただけです。タグ名からバージョン部分を ApplicationVersion という変数名に保存して、マニフェストと MSBuild のパラメータに渡しています。

実際に WinQuickLook で GitHub Release を作成してビルドを行ってみましたが、ちゃんと Desktop Bridge 側のバージョンとアセンブリ側のバージョンが一致した形で msixupload が作成されました。

f:id:shiba-yan:20200303180916p:plain

これまではアセンブリ側のバージョンは捨てて、Desktop Bridge 側だけ手動で変えていたのですが、GitHub Release と Azure Pipelines を使って自動化したのでコードとの関連付けとミスを防ぐことが出来ます。

アセンブリのバージョンも適切に設定されるようになったので、App Center からバージョン別にテレメトリを確認できるようになりました。

f:id:shiba-yan:20200305222619p:plain

これまでは全てが 1.0.0 になっていたのですが、UWP と同様の挙動になりました。クラッシュログや利用頻度をバージョン毎に確認できるようになって捗ります。

やはり Windows Store へのアップロードも自動で行いたくなってきました。暇なときにやります。