Mason Docs

Overview

Mason CLI is a powerful tool that allows developers to build describe a custom OS as configuration via a Mason Project, build that into an immutable artifact, and transform a fleet of devices into their own Smart Products. This power however doesn't come from the tool itself - but in the way you utilize it.

In this guide, we'll walk through integrating Mason CLI into a CI/CD pipeline to bring speed, efficency and stability to your product releases.

The guide is fairly extensive so if you want to skip to a finished example check this out. TLDR; the we pull down the Mason CLI with curl and run it automatically after our application builds complete!

What you'll learn

Masonifying your Android Project

The first step towards integrating Mason into your CI/CD pipeline is to add your Mason project configuration to your Android application project.

If you don't have a project configuration yet, checkout this guide to creating one.

If you're starting from scratch an easy way to do this is to simply run mason init in the root directory of your Android project. This command is interactive and can initialize a new project which results in a mason.yml and a .masonrc file being created inside of your project.

The mason.yml file is your project configuration. You can think of it as a manifest or description of the entire environment your application runs within. By adding it to your Android project and versioning it alongside your application source code you effectively expand your development capabilities from just the app to an entire operating system.

You can think of the .masonrc file as "configuration for Mason CLI itself" or a "helper to make using the CLI easier" by telling the CLI where all of your files are on your machine. This file is completely optional but we've found .masonrc to be especially helpful in an automated CI/CD environment (where you may have multiple apps building or unconventional file structures). Without it you would explicitly register any artifact associated with your project each time you made a new version:

mason register config /path/to/myconfiguration.yml
mason register apk /path/to/my/app1.apk
mason register apk /path/to/my/app2.apk
etc. 

When prompted update the path to your APK release artifiacts (we'll touch on signing in the build and release section). Our example application is written in ReactNative so that path will be android/app/build/outputs/apk. If you're following standard convention in Android Studio it will likely be apps/apk/release.

You can specify the path for each individual apk when registering artifacts as part of the release process if that is more suitable for your CI environment.

After running mason init our Andriod Project looks something like this:

VSCodeScreenshot

The project we're showing here is written in ReactNative and shown in VS code. If you're using Android Studio or another framework that's fine. A more advanced and finished example with an app written written in Kotlin can be found here.

If you have an existing Mason project you can use mason init as well, it will allow you to select your existing project and download the files! If you don't want to do this for any reason simply copy the yml file for your project into the root of your Android project.

Creating a Pipeline

We will be using Github Actions for this example however Mason CLI is agnostic of CI tools feel free to adapt this guide to your tool of choice such as Jenkins, Gitlab CI, etc.

This guide assumes you use Git for version control and the repository is hosted on GitHub. If you do not use Git/Github and want to follow along, this is a good starting point. Otherwise adapt for your tooling. Don't hesitate to contact support@bymason.com for assistance here! We're happy to create a new guide if you ask!

The cool thing about Github Actions is that you can define a pipeline for your project by adding a YML file to your project and describing the "workflow". Let's get started!

Visit Github's official documentation too learn Github Actions in more depth. Checkout their workflow templates to get started quickly.

We'll do this a bit manually so we can see how it is all working. Inside of our same Android project let's create a hidden .github directory at the root of our repository with a workflows subdirectory inside of it.

mkdir .github/
mkdir .github/workflows/

Now create our YML file inside of that newly created workflows directory.

touch .github/workflows/cicd.yml

You can name this file whatever you want but we'll call it cicd.yml. Then add this YML to the file and save it:

name: Build Android

on:
  push:
    branches:
      - production
jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v1
    - name: Run a one-line script
      run: echo Hello, Mason!

This is a simple hello world example just to get oriented and test that our actions are working. Your project should now look something like this:

Github Action hello world

Note: our branch is called main in this scenerio if you have a different branch that you want to push to change the name in the example above.

Commit this file and push it to your repo:

git add .github/
git commit -am"hello CI pipline\!"
git push origin main

If you'd like to skip this step go on ahead.

Thats it! You now have the beginnings of a CI/CD pipleine already setup and running! You should see something like this in your repo in the Actions tab:

Github Actions First Run

If you drill down into the output for the job you'll see our one line 'Hello, Mason!` output:

Github Actions First Run

Building and Releasing

In this section of the guide we'll update our cicd.yml to build a fully signed APK.

Though our app is written in ReactNative we will be using gradle to build the APK so it should be fairly applicable to a Java/Kotlin based project. A more advanced and finished example with an app written written in Kotlin can be found here.

The first thing we want to do in any CI/CD pipeline is run our unit tests. Since this app is a ReactNative app we'll first npm install dependencies and run npm test for our unit tests. With that, let's rename our first job in the pipeline to install-and-test and add run the appropriate npm commands.

Replace the entire contects of your cicd.yml file with this:

name: Build Android
on:
  push:
    branches:
      - main
jobs:
  install-and-test:
    runs-on: ubuntu-latest
    steps: 
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      - name: Install npm dependencies
        run: |
          npm install
      - name: Run tests
        run: |
          npm test

It may look a bit scary but have no fear! After running this dozens of times we've included the cache module to speed up our builds. You can leave that out if you wish for simplicity. The important bits are:

      - name: Install npm dependencies
        run: |
          npm install
      - name: Run tests
        run: |
          npm test

where we simply run npm install and npm test.

Next in our pipeline we'll want to perform the actual build. This gets a bit tricky as we will need to sign the APK too -- which involves having access to the signing key. Luckily, Github Actions has a well defined method for dealing with encrypted secrets so we can encrypt and decrypt our signing key within the pipeline.

If you already have a method for signing APKs for production releases in your CI environment feel free to skip this step.

For this we will use the gpg command line utility to encrypt our release keys and add them to as a our repo:

gpg --symmetric --cipher-algo AES256 my-release-key.keystore

Make sure to remember the pass phrase you use when GPG asks for one! We will reference this as ENCRYPT_PASSWORD going forward.

Next we need add to our secrets to our Github project. To do this navigate to the Settings tab in your repository and click on the "Secrets" side menu option.

Aside from the newly minted ENCRYPT_PASSWORD we also need to the actual keystore itself (it's a string so we can do that!) using the variable name KEYSTORE along with the typical keystore credentials and KEYSTORE_PASSWORD, KEY_ALIAS and KEY_PASSWORD.

In Github our secrets now look like this:

Secrets

Now that we have that all squared away, we'll add a new "job" to our workflow file called "build" with all of the nessesary logic to decrypt our key, execute the build and sign the resulting artifact. Our cicd.yml can be appended with :

  build:
    needs: install-and-test
    runs-on: ubuntu-latest
    steps: 
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      - name: Install npm dependencies
        run: |
          npm install
      - name: Decrypt keystore
        env:
          ENCRYPT_PASSWORD: ${{ secrets.ENCRYPT_PASSWORD }}
        run: |
          gpg --quiet --batch --yes --decrypt --passphrase="$ENCRYPT_PASSWORD" --output ./android/app/my-release-key.keystore ./android/app/my-release-key.keystore.gpg
      - name: Build Android Release
        env:
          KEYSTORE: ${{ secrets.KEYSTORE }}
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: |
          cd android && ./gradlew assembleRelease -x lintVitalRelease
      - name: Upload Artifact
        uses: actions/upload-artifact@v1
        with:
          name: app-release.apk
          path: android/app/build/outputs/apk

That final block you'll notice places app-release.apk in android/app/build/outputs/apk. The path we referenced in our .masonrc earlier! Perfect for our deploy job.

Deployment

Finally time to deploy our software! In this section of the guide we will be programatically downloading the latest release of Mason CLI, authenticating, and then deploying our project.

We suggest for this step to create a new user in Controller specfically for CI. We're working hard on allowing the CLI to be used exclusively with API keys - we'll update this guide when that is ready.

Once you have the user selected that you want to use for authentication store the email address and password for that Mason user as additional secrets in your repository.

Secrets

Once you've created the MASON_USERNAME and MASON_PASSWORD secrets let's add the last job to our workflow file. Update cicd.yml to:

  deploy_mason_dev:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Download outputs
        uses: actions/download-artifact@v1
        with:
          name: app-release.apk
          path: android/app/build/outputs/apk
      - name: Prep CLI
        run: |
          curl -Lo mason https://github.com/MasonAmerica/mason-cli/releases/download/1.7/mason-linux
          chmod +x mason
          ./mason login -u $MASON_USERNAME -p $MASON_PASSWORD
        env:
          MASON_USERNAME: ${{ secrets.MASON_USERNAME }}
          MASON_PASSWORD: ${{ secrets.MASON_PASSWORD }}
      - name: Register Mason project
        run: ./mason register -y project
      - name: Deploy Mason
        run: |
          ./mason deploy -py config iheartlives-hd latest dev

This seem like the simplest of jobs but it is the most powerful. There's a lot going on so let's walk through it.

First we grab the artifact from the previous build job

    - name: Download outputs
        uses: actions/download-artifact@v1
        with:
        name: app-release.apk
        path: android/app/build/outputs/apk

Then we pull down the Mason CLI using curl and authenticate

- name: Prep CLI
    run: |
        curl -Lo mason https://github.com/MasonAmerica/mason-cli/releases/download/1.7/mason-linux
        chmod +x mason
        ./mason login -u $MASON_USERNAME -p $MASON_PASSWORD
    env:
        MASON_USERNAME: ${{ secrets.MASON_USERNAME }}
        MASON_PASSWORD: ${{ secrets.MASON_PASSWORD }}

Finally we register the Mason project (it knows where our files are using .masonrc remember!) and deploy it to a group called dev

    - name: Register Mason project
        run: ./mason register -y project
    - name: Deploy Mason
        run: |
        ./mason deploy -py config iheartlives-hd latest dev

Publish to Github and watch your brand new CI/CD pipeline run!

Stay tuned for our guide on running automated end-to-end tests on real devices prior to deploying to production devices. Let us know interesting ideas here with X-Ray at your disposal or reach out with any requests!

Support

Next steps