Developers Rock 'n' Roll High School

C#、.NET MAUI、Xamarin、などの技術の備忘録 Azureとかも書く予定

.NET MAUIアプリをPipeline Buildする(Github ActionsでAndroid編)

前書き

  • .NET MAUIのPipeline Buildについて、.NET Blogに記事が掲載されていたので実際に試してみました。
  • .NET Blogの記事では、Github ActionsとAzure DevOpsそれぞれで各プラットフォーム(Androd、iOSWindows、macCatalyst)のBuildのやり方が記載されていますが、ここではひとつずつ取り上げていきたいと思います。(果たして全部書ききれるのだろうか。。)
  • Github Actions初めて使うので、ちょっと説明がくどくなるかもしれません。
  • 試した範囲は、署名されたapkファイルとaabファイルを作るところまでです。その先、Google Play ConsoleにUploadする方法についてははたくさん先達の方々たちの記事が存在するのでここではやりません。
  • 2021年からGoogle Play Storeへの登録はapkではなくaabが必須となりました。ただし、例えばSprint Reviewなんかでステークホルダーの人に動きを見てもらう時なんかにはapkがあったほうがいいので両方作成して取得できるようにします。

参考情報

試した結果

  • ソースコードの全量はこちらです。
  • 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 }}"
  • dotnet publish コマンドでBuildと署名をします。通常、MAUIのcsprojファイルにこんな感じで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をやってみるか考え中。。