GitHub Actions: Build a Xamarin.Forms project and create the APK

09-06-2020

In this post I'll show a way to configure a basic GitHub Action workflow in order to build a Xaramin.Forms application and create the APK, only Android at the moment.

First of all, the workflow:

name: Build

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  # Build App
  build:
    runs-on: windows-latest

    env:
      SERVICE_APP_KEY: ${{ secrets.SERVICE_APP_KEY }}

    steps:
      - uses: actions/checkout@v2

      - name: Setup MSBuild
        uses: microsoft/setup-msbuild@v1.0.0

      - name: Build Solution
        run: msbuild ./MyApplication.sln /restore /p:Configuration=Release

      - name: Create and Sign the APK
        run: msbuild MyApplication\MyApplication.Android\MyApplication.Android.csproj /t:SignAndroidPackage /p:Configuration=Release /p:OutputPath=bin\Release\

      - name: Upload artifact
        uses: actions/upload-artifact@v2
        with:
          name: MyApplication.apk
          path: MyApplication\MyApplication.Android\bin\Release\com.companyname.myapplication-Signed.apk

Step by step

The first lines are self-explanatory so I'll jump to the build section.

build:
  runs-on: windows-latest

This will configure a Windows machine and all steps for this job will run in this environment. The default shell will be a PowerShell, so we can run scripts in this language.

env:
  SERVICE_APP_KEY: ${{ secrets.SERVICE_APP_KEY }}

With this line we create an environment variable, SERVICE_APP_KEY with the content of the GitHub secret secrets.SERVICE_APP_KEY. The variables created in an upper level can be accessed by its children elements but a variable created in a step level, for example, cannot be accessed from other step or a parent element.

A secret in GitHub is a piece of encrypted information that belongs to the repository. To create a secret go to your repository Settings -> Secrets.
If you try to print the value of a secret during the build process you will only see something like: SERVICE_APP_KEY: ****

Now let's explain the steps.

steps:
  - uses: actions/checkout@v2

The first step is the checkout. This action will download all the code to the current machine.

- name: Setup MSBuild
  uses: microsoft/setup-msbuild@v1.0.0

The next steps is to use the action microsoft/setup-msbuild@v1.0.0 in order to configure the environment that allow us to use the msbuild tool.

- name: Build Solution
  run: msbuild ./MyApplication.sln /restore /p:Configuration=Release

Build the solution in Release mode.

- name: Create and Sign the APK
  run: msbuild MyApplication\MyApplication.Android\MyApplication.Android.csproj /t:SignAndroidPackage /p:Configuration=Release /p:OutputPath=bin\Release\

In order to create and sign the package we will execute the msbuild command over the Android project with the target /t:SignAndroidPackage. The configuration should be Release and the generated .apk file will be located in {project_folder}\bin\Release

The name of the file will be com.companyname.myapplication-Signed.apk. The default name of the package is com.companyname.myapplication, you can change it in the AndroidManifest.xml file or in the properties of the Android project in Visual Studio.

This process attach the -Signed suffix to the name.

- name: Upload artifact
  uses: actions/upload-artifact@v2
  with:
    name: MyApplication.apk
    path: MyApplication\MyApplication.Android\bin\Release\com.companyname.myapplication-Signed.apk

The last step is to make available the artifact to be downloaded. We will use the action actions/upload-artifact@v2 with the parameters name as the name of the artifact, and the path, the path where the package is located.

How to manage the secret keys

The hardcoded keys are not a good practice ;) A better solution could be create an environment variable in your local system, but, how to access from code? One approach I follow is to create a class to manage the secrets and populate it with placeholders, something like:

public static class Secrets{
  public static string ServiceID => "#KEY1#"
  public static string OtherKey => "#KEY2#"
}

To replace the placeholders by the valid keys I have a PowerShell script that runs in the pre-build event on Visual Studio.

function Replace-Text{
  <#
  .SYNOPSIS
    Replace the $placeholder value by $key value in $file
  .PARAMETER $file
    File to replace values. Mandatory
  .PARAMETER $placeholder
    Value to be replaced. Mandatory
  .PARAMETER $key
    The new value to replace the $placeholder. Mandatory
  .EXAMPLE
    Replace-Text file_path "#TEXT TO REPLACE#" "KEY VALUE"
  #>
  Param(
    [Parameter(Mandatory=$true)]
    [string]$file,
    [Parameter(Mandatory=$true)]
    [string]$placeholder,
    [Parameter(Mandatory=$true)]
    [string]$key
  )
  ((Get-Content -path $file -Raw) -replace $placeholder, $key) | Set-Content -Path $file
}
$file = $args[0]
$placeholder = $args[1]
$key_value = $args[2]
Replace-Text $file $placeholder $key_value

This script have three parameters:

  • the $file where the placeholder are
  • the $placeholder to be replaced
  • the $key_value from the environment variable

This script is called from the Pre-build event command line. Project properties -> Build Events:

powershell.exe -ExecutionPolicy Unrestricted $(ProjectDir)..\..\Tools\replace-keys-script.ps1 $(ProjectDir)Secrets.cs "`#KEY1`#" $env:SERVICE_APP_KEY

powershell.exe -ExecutionPolicy Unrestricted $(ProjectDir)..\..\Tools\replace-keys-script.ps1 $(ProjectDir)Secrets.cs "`#KEY2`#" $env:OTHER_KEY
If the placeholder has # characters like #INSERT_KEY_HERE# they must be escaped with grave-accents: "`#INSERT_KEY_HERE`#" in order to escape the comment symbol.

Let me show the folder structure:

/
|- Solution.sln
|- Project/
|- Project.Android/
|- Tools/
    |- replace-keys-script.ps1

Why to use $(ProjectDir)..\..\Tools instead of $(SolutionDir)Tools? Because $(SolutionDir) does not works on GitHub Actions for wharever reason.

As you may notice, this process change the content of the files and Git wil mark them. To fix this I do a checkout for all files in the Post-build event:

git checkout $(ProjectDir)Secrets.cs

After the build process the application will work with correct values and the code remains intact.

The only thing we need to do in GitHub is to create the secrets we need for the keys and create the environment variables in the workflow with the same name as we have in out system.

References

Xamarin Android - Build Process
GitHub Action - setup-msbuild
Prebuild event in Visual Studio replacing $(SolutionDir) with *Undefined*
How to use VisualStudio SignAndroidPackage in TeamCity?