marc walter

Configure CircleCI to build and sign an Android app

2019-07-11

For the TonUINO Android app I wanted to setup a CI system that also creates github releases.

The initial configuration worked (and is triggered after I push a vX.Y.Z tag to the repository) at first, but unfortunately an Android app must be signed before it can be deployed to and executed on an actual device.

So this is how I set up signing on CircleCI in addition to compilation, test execution and creating apk releases.

Signing

Create a keystore, then convert it to a base64 string to be able to upload it to CircleCI

$ base64 keystore.jks --wrap=0

Then store it as environment variable KEYSTORE in the CircleCI environment config.

After that, also store the keystore password, the key name and the key password as an environment config.

Working configuration

app/build.gradle

android {
    compileSdkVersion 28
    defaultConfig { ... }
    signingConfigs {
        ci {
            storeFile file('keystore.jks')
            storePassword System.getenv('TONUINO_KEYSTORE_PASSWORD')
            keyAlias = System.getenv('TONUINO_KEY')
            keyPassword System.getenv('TONUINO_KEY_PASSWORD')
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.ci
        }
    }
}

.circleci/config.yml

version: 2
jobs:
  build:
    working_directory: ~/code
    docker:
      - image: circleci/android:api-28-alpha
    environment:
      JVM_OPTS: -Xmx3200m
    steps:
      - checkout
      - restore_cache:
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Chmod permissions #if permission for Gradlew Dependencies fail, use this.
          command: sudo chmod +x ./gradlew
      - run:
          name: Download Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}

      - run:
          name: Run Tests
          command: ./gradlew lint test
      - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
          path: app/build/test-results

      - run:
          name: write keystore.jks # from environment variable
          command: echo $TONUINO_KEYSTORE | base64 -di | tee keystore.jks app/keystore.jks >/dev/null

      - run:
          name: Build apk
          command: ./gradlew assembleRelease
      - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
          path: app/build/reports
          destination: reports
      - run:
          name: Copy apk
          command: |
            mkdir -p dist
            cp -r app/build/outputs/apk/release/*.apk dist/tonuino-nfc-tools.apk
      - store_artifacts:
          path: dist
          destination: dist
      # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples

      # https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
      # Persist the specified paths (workspace/echo-output) into the workspace for use in downstream job.
      - persist_to_workspace:
          # Must be an absolute path, or relative path from working_directory. This is a directory on the container which is
          # taken to be the root directory of the workspace.
          root: ./
          # Must be relative path from root
          paths:
            - dist

  publish-github-release:
    docker:
      # based off alpine docker image
      - image: cibuilds/github:0.10
    steps:
      - attach_workspace:
          at: persisted
      - run:
          name: "list files"
          command: |
            apk add tree
            pwd
            tree persisted
      - run:
          name: "Publish Release on GitHub"
          command: |
            VERSION=${CIRCLE_TAG}
            ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} persisted/dist

workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build:
          filters:  # required since `deploy` has tag filters AND requires `build`
            tags:
              only: /.*/
      - publish-github-release:
          requires:
            - build
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/