Привет, Хаброжители! На днях начал изучать GitHub Actions для Android. Ранее у меня была удачная попытка настройки данного функционала для проекта на Flutter, но без деплоя, для которого полно информации и гайдов как на англоязычных ресурсах, так и на русскоязычных, а вот с нативным андроидом не всё так прозаично. И решил записать основные проблемы и их решение.

Этап первый: настройка для автоматической подписи готового apk


Первая проблема с которой я столкнулся, это сделать универсальную настройку, позволяющую собирать релизные варианты как локально, так и глобально.

Я использую вариант с использование файла keystore.properties, который позволяет нам добавить ключ разработчика в папку проекта, не светя при этом паролями от него, делается это так:

apply plugin: ...

def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
  ...

  signingConfigs {
    release {
        storeFile file("../MyKey.jks")
        storePassword keystoreProperties['RELEASE_STORE_PASSWORD']
        keyAlias keystoreProperties['RELEASE_KEY_ALIAS']
        keyPassword keystoreProperties['RELEASE_KEY_PASSWORD']
    }
    debug {
        storeFile file('../debug.keystore')
    }
  }

  buildTypes {
    release {
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        minifyEnabled false
        signingConfig signingConfigs.release
        buildConfigField "String", "PIN_ALIAS", keystoreProperties['PIN_ALIAS']
        buildConfigField "String", "DB_PASS_ALIAS", keystoreProperties['DB_PASS_ALIAS']
    }
    debug {
        minifyEnabled false
        signingConfig signingConfigs.debug
        buildConfigField "String", "PIN_ALIAS", keystoreProperties['PIN_ALIAS']
        buildConfigField "String", "DB_PASS_ALIAS", keystoreProperties['DB_PASS_ALIAS']
    }
  }
}

dependencies {
  ...
}

И тут возникла проблема, как сделать так, что бы мы могли взять ключи из ${{ secrets.MY_KEY }} и при этом градл понимал, если у нас есть keystore.properties, то берём из него, если нет то берём из секретов? Решение нашлось на одном из гайдов для флаттера, где для этого они использую окружения (Кстати, здесь классный подход, чтобы не светить нашим ключём разработчика), но проблему это не решило. Перепробовав несколько вариантов с введением дополнительных файлов и т.п., остановился на самом простом: мы вводим дополнительно несколько переменных(в зависимости от нужного нам количества), и проверяем наличие файла keystore.properties:

def release_store_password
def release_key_password
def release_key_alias
def pin_alias
def db_pass_alias

def keystoreProperties = new Properties()
if (rootProject.file("keystore.properties").exists()) {
    keystoreProperties.load(new FileInputStream(rootProject.file("keystore.properties")))
    release_store_password = keystoreProperties['RELEASE_STORE_PASSWORD']
    release_key_password = keystoreProperties['RELEASE_KEY_PASSWORD']
    release_key_alias = keystoreProperties['RELEASE_KEY_ALIAS']
    pin_alias = keystoreProperties['PIN_ALIAS']
    db_pass_alias = keystoreProperties['DB_PASS_ALIAS']
} else {
    release_store_password = System.env.RELEASE_STORE_PASSWORD
    release_key_password = System.env.RELEASE_KEY_PASSWORD
    release_key_alias = System.env.RELEASE_KEY_ALIAS
    pin_alias = System.env.PIN_ALIAS
    db_pass_alias = System.env.DB_PASS_ALIAS
}

android {
   signingConfigs {
        release {
            storeFile file("../my_key.jks")
            storePassword = release_store_password
            keyAlias = release_key_alias
            keyPassword = release_key_password
        }
    buildType{
       release {
          buildConfigField "String", "PIN_ALIAS", "\"$pin_alias\"" //если вам нужно ввести некоторые 
          buildConfigField "String", "DB_PASS_ALIAS", "\"$db_pass_alias\"" // дополнительные данны.
      }
    }
}

Итак, теперь наш сборщик умеет собирать и сразу подписывать наш apk.

Этап второй: версия сборки.


Тут нет ничего сверх естественного, хотелось получить какой-то, достаточно универсальный вариант, минимальной сложности. Погуглив, присмотрелся, сколько разработчиков — столько и вариантов и каждый извращается как может. Мне какие-то сверх сложные подходы не нужны и я уже хотел было использовать BUILD_NUMBER, но тут я наткнулся на параметр у для GitHub actions: ${{ github.run_number }}.

${{ github.run_number }}
Уникальный номер для каждого запуска определенного рабочего процесса в хранилище. Это число начинается с 1 для первого запуска рабочего процесса и увеличивается с каждым новым запуском. Это число не изменится, если вы повторно запустите рабочий процесс. (Запуском здесь подразумевается — когда вы пушите в ветку).

По этому взвесив все за и против имеем следующее решение:

def versionPropsFile = rootProject.file('version.properties')
Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
def verCode = versionProps['VERSION_CODE'].toInteger()

android {
  defaultConfig {
    versionCode verCode
    versionName "1.1.$verCode"
  }
}

//version.properties файл
VERSION_CODE=1

В рабочем процессе делаем так:

- name: Output version code
        run: echo VERSION_CODE=${{ github.run_number }} > ./version.properties

Этап третий: развертывание (deploy)


На данный момент я нашел два готовых решения: Gradle Play Publisher и Upload Android release to the Play Store

Первый вариант отпал по причине: использование гаубицы при стрельбе по воробьям. По этому я выбрал второй. Ничего вроде сложного в нём нет, а тут есть подробная инструкция: Тут.


- name: Upload to PlayMarket
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.guralnya.notification_tracker
          releaseFile: app/build/outputs/apk/release/notification_tracker.release.apk
          track: beta
          userFraction: 0.33
          whatsNewDirectory: distribution/whatsnew

Но некоторые моменты у меня всё же возникли:

  • serviceAccountJson и serviceAccountJsonPlainText — с первым я так и не разобрался в каком виде его нужно положить в секреты, второй же просто берём содержимое файла и кладём в наш секрет.
  • releaseFile — использовал самый простой подход, когда мы берём готовый файл из папки с проектом, но вариант со звёздочкой не прокатил: notification_tracker.release.*.apk, где у меня стоит время сборки. Хотя в другом экшене, который у меня используется для загрузки файла (actions/upload-artifact@v2), такой подход работал отлично.
  • whatsNewDirectory — внимательнее к языковым кодам. Если английский я взял из гугл-консоли при добавлении новой версии (en-IN), а Русский как (ru-RU), то логично предположить что все языки работают по том же принципу, но нет — Украинский я не доглядел, а он там помечен как (uk), потому если не хотите лишний раз комититься и видеть красный крестик, лучше свериться с той же консолью.

Есть ситуация, когда у вас может по какой-то причине не деплоиться, это когда вы ещё не опубликовали приложение (встречать не приходилось, но в одной статье было описано). Так что начинайте с того, что сначала опубликуйте приложение в ручную если вы ещё этого не сделали.

Итоговый рабочий процесс — будет оптимизироваться и улучшаться вместе с файлом градла


Android CI.yaml:


name: Android_CI

on:
  push:
    branches:
      - beta_release

jobs:
  build:
    runs-on: ubuntu-latest
    name: Build release-apk and deploy to PlayMarket
    steps:
      - uses: actions/checkout@v2
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      # Without NDK not compile and not normal error message. NDK is required
      - name: Install NDK
        run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}
      # Some times is have problems with permissions for ./gradle file. Then uncommit it code
      #    - name: Make gradlew executable
      #      run: chmod +x ./gradlew
      - name: Output version code
        run: echo VERSION_CODE=${{ github.run_number }} > ./version.properties
      - name: Build with Gradle
        run: ./gradlew assemble
        env:
          RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
          RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
          RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
          PIN_ALIAS: ${{ secrets.PIN_ALIAS }}
          DB_PASS_ALIAS: ${{ secrets.DB_PASS_ALIAS }}
      - name: Upload APK
        uses: actions/upload-artifact@v2
        with:
          name: notification_tracker
          path: app/build/outputs/apk/release/notification_tracker.release.apk
      - name: Upload to PlayMarket
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.guralnya.notification_tracker
          releaseFile: app/build/outputs/apk/release/notification_tracker.release.apk
          track: beta
          userFraction: 0.33
          whatsNewDirectory: distribution/whatsnew

Важный момент


— необходимость NDK. Без установленного NDK у вас не соберётся проект, по крайней мере релизный. Можно долго гадать в чём проблема и искать решение, так как нормального сообщения ошибки нет. Иногда можно отловить вот это: Task :app:stripDebugDebugSymbols FAILED. После гуглинга и экспериментов, оказалось что нет NDK. Делаем так:

- name: Install NDK
        run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}

Или лучше добавить в Gradle (спасибо пользователю anegin):

defaultConfig {
  ndkVersion '21.2.6472646'
}

P.S. Для Gradle использовал подсветку кода от Kotlin. Для YAML — от JSON. Конечно немного не то, но лучше мне найти не удалось, если есть лучшие варианты, сообщите мне пожалуйста и я исправлю.

P.S.S. Может быть у кого есть лучшее решение или предложения по улучшения, напишите их в комментариях, так как по первому этапу вопрос провисел на StackOverflow больше 10-ти дней, но ответа так и не последовало.