.NET MAUIアプリをPipeline Buildする(Github ActionsでAndroid編)
前書き
- .NET MAUIのPipeline Buildについて、.NET Blogに記事が掲載されていたので実際に試してみました。
- .NET Blogの記事では、Github ActionsとAzure DevOpsそれぞれで各プラットフォーム(Androd、iOS、Windows、macCatalyst)のBuildのやり方が記載されていますが、ここではひとつずつ取り上げていきたいと思います。(果たして全部書ききれるのだろうか。。)
- Github Actions初めて使うので、ちょっと説明がくどくなるかもしれません。
- 試した範囲は、署名されたapkファイルとaabファイルを作るところまでです。その先、Google Play ConsoleにUploadする方法についてははたくさん先達の方々たちの記事が存在するのでここではやりません。
- 2021年からGoogle Play Storeへの登録はapkではなくaabが必須となりました。ただし、例えばSprint Reviewなんかでステークホルダーの人に動きを見てもらう時なんかにはapkがあったほうがいいので両方作成して取得できるようにします。
参考情報
- .NET Blog
- Pipeline Buildについて書かれています。
- サンプルのソースコード
- ↑の.NET Blogのサンプルのソースコードです。
- 動画コンテンツ
- 上記の.NET Blogの記事を執筆したSweeky Satpathyさんが.NET ConfのYoutube動画で配信しています。
- MS Docs
- Androidアプリの発行方法(Pipelineは使用しないパターン)が記載されています。
試した結果
- ソースコードの全量はこちらです。
- workflowのymlファイルはこちらです。
- 作ったアプリは↓の感じです。動きは.NET MAUIのテンプレートそのままですね。なんとなく中身はViewModelとModelを別Projectに切り出してますが、これはまあ本題ではないです。
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModel="clr-namespace:PipelineSampleCore.ViewModels;assembly=PipelineSampleCore" x:Class="DotnetMauiApp.MainPage" x:DataType="viewModel:MainViewModel"> <ScrollView> <VerticalStackLayout Spacing="25" Padding="30,0" VerticalOptions="Center"> <Image Source="dotnet_bot.png" SemanticProperties.Description="Cute dot net bot waving hi to you!" HeightRequest="200" HorizontalOptions="Center" /> <Label Text="Hello, World!" SemanticProperties.HeadingLevel="Level1" FontSize="32" HorizontalOptions="Center" /> <Label Text="Welcome to .NET Multi-platform App UI" SemanticProperties.HeadingLevel="Level2" SemanticProperties.Description="Welcome to dot net Multi platform App U I" FontSize="18" HorizontalOptions="Center" /> <Button x:Name="CounterBtn" Text="{Binding CounterButtonText}" SemanticProperties.Hint="Counts the number of times you click" HorizontalOptions="Center" Command="{Binding CountUpCommand}" /> </VerticalStackLayout> </ScrollView> </ContentPage>
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PipelineSampleCore.Models; namespace PipelineSampleCore.ViewModels; [ObservableObject] public partial class MainViewModel { readonly ClickCounter _counter; public string CounterButtonText => _counter.CountText; public MainViewModel(ClickCounter counter) => _counter = counter; [RelayCommand] void CountUp() { _counter.CountUp(); OnPropertyChanged(nameof(CounterButtonText)); } }
namespace PipelineSampleCore.Models; public class ClickCounter { int _counter = 0; public string CountText { get; private set; } = "Click me"; public void CountUp() { _counter++; CountText = _counter == 1 ? $"Clicked {_counter} time" : $"Clicked {_counter} times"; } }
Setup
- ymlファイルの内容を見ていきたいと思います。まずはBuildの前準備の箇所からです。ここはほぼ.NET Blogの記事通りです。
name: WindoesCI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] workflow_dispatch: env: DOTNETVERSION: 6.0.400 BASE64_KYESTORE: ./android/release.keystore.base64 DECRYPTED_KEYSTORE: ./android/release.decrypted.keystore DECRYPTED_KEYSTORE_BACKSLASH: ..\\android\\release.decrypted.keystore jobs: buildAndroid: runs-on: windows-2022 steps: - uses: actions/checkout@v3 - name: Setup .NET SDK ${{env.DOTNETVERSION}} uses: actions/setup-dotnet@v2 with: dotnet-version: '${{env.DOTNETVERSION}}' - name: List installed .NET info shell: pwsh run: dotnet --info - name: Install .NET MAUI shell: bash run: | dotnet nuget locals all --clear dotnet workload install maui --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json dotnet workload install android --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json - name: Restore nuget packages run: | dotnet restore ./DotnetMauiApp/DotnetMauiApp.csproj dotnet restore ./PipelineSampleCore/PipelineSampleCore.csproj dotnet restore ./PipelineSampleTest/PipelineSampleTest.csproj
Unit Test
- Unit Testについてもほぼ.NET Blogに書かれているとおりです。私はViewModelとModelを別Project(PipelineSampleCore)に切り出したので、そこだけちょっと違います。
- name: Build and Run UnitTests shell: bash run: | dotnet build ./PipelineSampleCore/PipelineSampleCore.csproj dotnet build ./PipelineSampleTest/PipelineSampleTest.csproj dotnet test ./PipelineSampleTest/PipelineSampleTest.csproj --no-build --verbosity normal
- 下記のstepは、Unit Testの結果をいい感じに可視化してくれるものです。Build完了後にActionsの画面から確認できます。
- name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() with: name: Service Tests Summary path: ./PipelineSampleTest/TestResults/*.trx reporter: dotnet-trx
- テストコードはとりあえずこんな感じに書きました。ここも本題ではないので適当です。
using PipelineSampleCore.Models; namespace PipelineSampleTest; public class UnitTest1 { [Fact] public void Test1() { var counter = new ClickCounter(); Assert.Equal("Click me", counter.CountText); counter.CountUp(); Assert.Equal("Clicked 1 time", counter.CountText); counter.CountUp(); Assert.Equal("Clicked 2 times", counter.CountText); counter.CountUp(); Assert.Equal("Clicked 3 times", counter.CountText); } }
署名付きでビルド
- アプリへの署名は、Google Play Storeに出すときに必要になります。逆に開発段階で検証用のスマホにインストールするだけとかだったら必要ないので、例えばBranch運用次第でdevelop branchの場合は署名なしでビルドするとかにしておいたほうがよりクイックになるかもしれません。
- apkファイルとaabファイルに署名するには、あらかじめkeytstoreファイルを作成しておかなくてはいけません。keystoreファイルはそのアプリでずっと使い続けるもので、ローカルPCとかであらかじめ作成してどこかに保存しておくのですが、PublicなGitリポジトリの場合pushしてしまうとセキュリティ上よろしくないので事故らないように.gitignoreに記載しておいたほうが良いと思います。
- PublicなリポジトリでBuildする際にどうやってkeystoreファイルを参照するかというと、GithubのSecret機能を利用します。ポイントとしてGithubのSecretには文字列しか登録できないので、あらかじめファイルをbase64文字列化して登録し、Pipelineの処理でそれをdecodeします。(Azure Key Vaultとかセキュアな場所にファイルを置くという手もありますが、Loginして取りに行くのは手間。)
- base64文字列化する方法は、私の場合Windows PCのWSL2(Ubuntu)に入っているbase64コマンドを使いました。たぶん他のだいたいのディストリビューションでも入ってますし、GitBashにも入ってます。例えば「DotnetMauiApp.keystore」というkeystoreファイルをあらかじめ作成していたとした場合こんな↓感じでコマンド打つとbase64化された文字列が表示されます。
base64 DotnetMauiApp.keystore
- ↑の結果の文字列をコピってSecretに貼り付けます。keystoreのパスワードもここに入れておくとよいでしょう。(リポジトリの「Settings」タブからSecretの登録画面にいけます)
- secretに登録した値を取り出し、decodeしてファイル化します。
- name: Extract Android signing key from env shell: bash run: | mkdir -p android echo "${{ secrets.RELEASE_KEYSTORE }}" > "${{ env.BASE64_KYESTORE }}" base64 -d "${{ env.BASE64_KYESTORE }}" > "${{ env.DECRYPTED_KEYSTORE }}"
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'"> <AndroidKeyStore>True</AndroidKeyStore> <AndroidSigningKeyStore>myapp.keystore</AndroidSigningKeyStore> <AndroidSigningKeyAlias>key</AndroidSigningKeyAlias> <AndroidSigningKeyPass></AndroidSigningKeyPass> <AndroidSigningStorePass></AndroidSigningStorePass> </PropertyGroup>
- しかし、秘密にすべき情報を直書きはよろしくないので、↓のようにdotnet publishコマンドのパラメーターとして渡します。
- name: Build Android App shell: bash run: | dotnet publish ./DotnetMauiApp/DotnetMauiApp.csproj -f:net6.0-android -c:Release //p:AndroidSigningKeyPass="${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" //p:AndroidSigningStorePass="${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" //p:AndroidSigningKeyStore="${{ env.DECRYPTED_KEYSTORE_BACKSLASH }}" //p:AndroidSigningKeyAlias=key
- dotnet publish コマンドでapkとaabが作成されるので、あとはそれをDLできるようにするだけです。(どの場所にどんな名前でapkやaabが作成されるか確認したい場合は、Local PCでdotnet publishしてみるといいかと思います。)
- uses: actions/upload-artifact@v3 with: name: artifact-android path: | ./DotnetMauiApp/bin/Release/net6.0-android/publish/*.apk ./DotnetMauiApp/bin/Release/net6.0-android/publish/*Signed.aab
- Pipelineが完走後、ここを押すとapkとaabが入ったzipをDLできます。
署名の確認
- 一応、署名がちゃんとされてるか確認してみます。いったんbase64したりしたのでちゃんと署名できているか不安になりますね。
- 方法は、keystoreファイルのフィンガープリントとapkファイルのフィンガープリントが一致しているかを確認します。
keystoreファイルの確認
keytool -v -list -keystore DotnetMauiApp.keystore
apkの確認
keytool -printcert -jarfile com.companyname.dotnetmauiapp-Signed.apk
- こんな感じでそれぞれ表示されるはずなので、一致しているか確認します。
証明書のフィンガプリント: SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX SHA256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
あとがき
- 無事にGithub ActionsでAndroidアプリをビルドできるようになりました。この程度だと全然難しくはなかったですね。(dotnet publishのパラメーター渡すときのスラッシュの扱いにちょっとはまりましたが。。) Github Antions初めて書きましたが、単品のアプリをBuildする程度であればさくっと書けることがわかりました。おそらく本番プロダクトでは他のクライアントアプリやバックエンドもあってもっと壮大なPipelineを書かないといけなくなると思うので、今後もキャッチアップを進めて知見をつけていきたいと思います。次はiOSの記事を書くか、それともAndroidでAzure DevOpsをやってみるか考え中。。