В этой очень небольшой заметке я расскажу об очень небольшом усовершенствовании процесса автоматической сборки приложения в Travis CI. Я это проделал на примере Андроид-приложения, но, естественно, это будет работать и для других языков. Постановка задачи очень проста — участники сообщества попросили автоматически собирать в выкладывать приложение после каждого коммита в репозитории на GitHub. То есть речь идёт не о сборке фиксированных версий, а именно о «ежедневных» сборках, которые можно сразу же установить и тестировать, не дожидаясь официальной версии. Я, как разработчик, подобную заинтересованность могу только приветствовать, так как это сильно повышает качество обратной связи. Реализация этого процесса очень проста, только штатные средства GitHub и Travis CI, никакой магии. Так что я до сих пор сомневаюсь, стоит ли вообще о таком писать и отвлекать уважаемых хаброжителей от более серьёзных тем. Но если кто заинтересовался — прошу под кат.


Я, как разработчик под Android, с интересом и удовольствием мониторю некоторые Open Source проекты на GitHub, которые прошли успешное испытание временем и активно развиваются, например: good-weather, AFWall+, Timber, Pedometer, AmazeFileManager, ConnectBot, K-9 Mail.


У всех этих репозиториев есть два общих момента — в них настроена автоматическая сборка приложения на сервере Travis CI после каждого коммита, и результаты этой автоматической сборки так и остаются на сервере Travis CI, то есть собранное проложение просто удаляется. Я же, как уже сказал во введении, хочу извлечь пользу из этого собранного APK-файла и поместить его обратно в GitHub репозиторий, чтобы он был сразу же доступен участникам сообщества для тестирования.


Travis CI предоставляет штатный метод загрузки собранного приложения на GitHub, но он предназначен для работы с тегами, то есть эта сборка, во первых, запускается при создании нового тега в GitHub-репозитории, и, во-вторых, позволяет загрузить APK-файл только в раздел GitHub Releases, но не в ветку репозитория. Так как создавать тег для каждого коммита — это, на мой взгляд, насилие над здравым смыслом, то этот метод я отбросил как несоответствующий духу и сути задачи.


Командный файл Travis CI (.travis.yml), расположенный в корне репозитория, имеет простую структуру:


language: android

jdk: oraclejdk8

android:
  components:
  - platform-tools
  - tools
  - build-tools-25.0.2
  - android-25
  - extra-android-m2repository

branches:
  only:
  - master

install:
  - chmod +x ./gradlew

script: ./gradlew clean assembleDebug

notifications:
  email:
    on_success: change
    on_failure: always

Этот скрипт выполняется на виртуальной машине в корне git-репозитория, который клонирован в т.н. "detached HEAD" режиме, то есть не позволяет напрямую закоммитить что-либо в мастер-ветку удалённого (то есть оригинального) GitHub-репозитория.
Если внимательно посмотреть лог выполнения этого скрипта на виртуальной мишине, то в самом начале (секция git скрипта, которая в данном примере не сконфигурирована) Travis делает вот что:


$ git clone --depth=50 --branch=master https://github.com/user/repo.git user/repo
Cloning into 'user/repo'...
$ cd  user/repo
$ git checkout -qf d7d29a59cef70bfce87dc4779e5cdc1e6356313a

Именно git checkout -qf и переводит локальную ветку в "detached HEAD" режим.
После того, как секция script отработала (в моём примере ./gradlew clean assembleDebug), и в директории ./app/build/outputs/apk появился сгенерированный APK-файл, вызывается секция after_success, где можно средствами Git закоммитить этот файл. Вопрос только, куда?


Есть несколько вариантов.


1) Можно использовать GitHub-Pages и помещать APK-файл туда, то есть коммитить в ветку gh-pages. Основной минус такого подхода в том, что GitHub-Pages рассчитаны на конечных пользователей, которые должны загружать приложение из официальных магазинов. Участники сообщества работают всё же с самим репозиторием, а не с GitHub-Pages. Поэтому я такой вариант не рассматриваю.


2) Можно коммитить обратно в мастер-ветку GitHub-репозитория, например, в папку autobuild. В этом случае, нужно деактивировать "detached HEAD", закоммитить файл, авторизоваться в удалённом репозитории и выполнить push.


install:
  - git checkout master
  - chmod +x ./autobuild/push-apk.sh
after_success:
  - ./autobuild/push-apk.sh

где push-apk.sh выглядит так:


#!/bin/sh
mv ./app/build/outputs/apk/snapshot.apk ./autobuild/
git config --global user.email "travis@travis-ci.org"
git config --global user.name "Travis CI"
git remote add origin-master https://${AUTOBUILD_TOKEN}@github.com/user/repo > /dev/null 2>&1
git add ./autobuild/snapshot.apk
# We don’t want to run a build for a this commit in order to avoid circular builds: 
# add [ci skip] to the git commit message
git commit --message "Snapshot autobuild N.$TRAVIS_BUILD_NUMBER [ci skip]"
git push origin-master

В данном варианте после каждого коммита в мастер-ветке GitHub-репозитория Travis будет делать ещё один коммит, где файл snapshot.apk будет также помещён в мастер-ветку. С одной стороны, удобно, что все в одном месте. С другой, этот файл также нужно постоянно синхронизировать в локальных репозиториях, что не очень удобно для разработчиков.


3) Посте всех экспериментов мне больше всего понравился третий вариант. В репозитории создаётся ветка autobuild, но из неё удаляются все файлы и директории за исключением папки autobuild. Этот огрызок полноценной веткой не является, так как её нельзя синхронизировать с мастер-веткой. Скрипт push-apk.sh будет выглядеть в этом случае так:


#!/bin/sh

# Checkout autobuild branch
cd ..
git clone https://github.com/user/repo.git --branch autobuild --single-branch repo_autobuild
cd repo_autobuild

# Copy newly created APK into the target directory
mv ../repo/app/build/outputs/apk/snapshot.apk ./autobuild

# Setup git for commit and push
git config --global user.email "travis@travis-ci.org"
git config --global user.name "Travis CI"
git remote add origin-master https://${AUTOBUILD_TOKEN}@github.com/user/repo > /dev/null 2>&1
git add ./autobuild/snapshot.apk

# We don’t want to run a build for a this commit in order to avoid circular builds: 
# add [ci skip] to the git commit message
git commit --message "Snapshot autobuild N.$TRAVIS_BUILD_NUMBER [ci skip]"
git push origin-master

Последняя пара слов про авторизацию. За неё отвечает переменная окружения AUTOBUILD_TOKEN. Эта переменная задаётся в разделе


env:
  global:
    secure: 

Данный раздел содержит зашифрованный персональный ключ, который нужно сгенерировать на странице Personal access tokens. После чего он шифруется и добавляется в файл .travis.yml с помощью утилиты travis:


sudo gem install travis
echo AUTOBUILD_TOKEN=<Personal access token> | travis encrypt --add -r user/repo

Вот такое небольшое усовершенствование. Кому интересно, можно посмотреть рабочий вариант в этом репозитории.
Удачного всем прогрева серверов непрерывной интеграции!

Комментарии (10)


  1. Artem_zin
    18.09.2017 04:24
    +1

    Вы можете сделать chmod +x на нужные файлы локально и закоммитить это в гит (он умеет трекать execution permission), нет необходимости делать это в .travis.yml :)


  1. mkulesh Автор
    18.09.2017 09:17

    Спасибо за полезное добавление. Просто осталась старая привычка со времён SVN, поэтому и добавляю на автомате chmod +x куда следует и не следует.


  1. lieff
    18.09.2017 11:50

    Можно еще через секцию deploy, я предпочитаю именно так:

    deploy:
      - provider: releases
        api_key:
          secure: [hash]
        file:
          - linux.zip
          - win.zip
        skip_cleanup: true
        on:
          condition: $TRAVIS_OS_NAME == linux
          tags: true
    


    1. ozkriff
      18.09.2017 11:56

      В четвертом абзаце об этом говорится, но автор почему-то считает нелогичным работать через метки и выпуски, а хочет грузить двоичные артефакты в ветку самого репозитория.


      1. lieff
        18.09.2017 12:20

        Это если провайдер releases, но их больше и параметр tags отключаемый, так же другие провайдеры, которые тут не поддерживаются, предоставляют свои готовые скрипты как залить им (например hockeyapp). В releases, кстати, после удалении метки файлы оставались, так что может и залить без метки может, но я не пробовал.


    1. mkulesh Автор
      18.09.2017 12:13

      То есть Вы действительно создаете новый тег на каждый коммит и после каждого коммита создаете новую версию в разделе Release?


      1. lieff
        18.09.2017 12:22

        Нет, для ежедневных сборок используем hockeyapp, оттуда уже всем тестировщикам рассылается автоматом.


        1. mkulesh Автор
          18.09.2017 12:27

          Так моя заметка и посвящена именно ежедневным сборкам. Просто я вместо hockeyapp или другого внешнего провайдера использую специальную ветку самого проекта на GitHub.


          1. Artem_zin
            20.09.2017 11:01
            +1

            Для ежедневных (читай оч частых) сборок действительно лучше не захламлять репозиторий тегами, иначе будет боль как в https://github.com/JetBrains/kotlin/releases :(


  1. ghost404
    21.09.2017 10:35

    Мне не очень понятно почему вы делаете билд на каждый коммит. Вы же с git работаете и билд у вас на push запускается. Зачем вам пушить каждый коммит в отдельности? Может вы ещё от svn не отвыкли?


    С гитом обычно так — поработали немного, сделали несколько коммитов, дошли до какой-то логической точки и запушили.
    Разве вы делаете не так?


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


    Проблема засорения репозитория, как в примере от Artem_zin, связана с тем, что у них не очень удачно выбран алгоритм релизов и именование меток. Вместо:


    • build-1.1.60-dev-17
    • build-1.1.60-dev-18
    • build-1.1.60-dev-19

    Можно былоб писать проще


    • 1.1.60.17
    • 1.1.60.18
    • 1.1.60.19

    Последняя цифра это номер сборки в текущем релизе. Релизы сборки можно создавать автоматом на Travis CI. Не обязательно явно делать релиз.


    Если релизы создавать автоматом, то может возникнуть проблема, когда захочется сделать полноценный релиз. Придется пошаманить немного.


    А теперь о главном. О преимуществах подхода через релизы и о недостатке вашего решения.
    Проблема в том, что у вас сборка хранится в репозитории и отличить одну от другой можно только по хешу коммита. После того как пользователь скачает сборку, он ни как не сможет определить какая у него версия. Он ни как не сможет определить на сколько версия, скачанной им сборки, устарела. Если он обнаружит багу в приложении, то он ни как не сможет вам сказать в какой версии произошла ошибка. Всё что он может, это приложить к тикету архив со сборкой и вам уже придется ручками, по хошсумме файла искать сборку в истории коммитов.


    Возможно я утрирую и есть какой-то более человеческий способ.


    Я же лишь хочу сказать, что возможно для контроля удобнее выкладывать не файл myapp.apk, а myapp_1.1.60.19.apk или myapp_1.1.60.19.zip.


    Я не критикую, я только предлагаю.