Travis CI: автоматическая загрузка собранных модулей на GitHub


    В этой очень небольшой заметке я расскажу об очень небольшом усовершенствовании процесса автоматической сборки приложения в 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

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

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

    Подробнее
    Реклама
    Комментарии 12
    • +1

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

      • 0

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

        • 0
          Можно еще через секцию deploy, я предпочитаю именно так:
          deploy:
            - provider: releases
              api_key:
                secure: [hash]
              file:
                - linux.zip
                - win.zip
              skip_cleanup: true
              on:
                condition: $TRAVIS_OS_NAME == linux
                tags: true
          
          • 0

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

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

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

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

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

          • +1

            Мне не очень понятно почему вы делаете билд на каждый коммит. Вы же с 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.


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

            • 0

              Спасибо за вдумчивый и развёрнутый комментарий. На первый взгляд, Вы все пишите правильно. Лишь два момента не дают мне покоя.


              Во-первых, Вы странным образом отождествляете git с двухстадийной методикой работы и противопоставляете эту методику svn. Точно так же, как и в git, двухстадийные коммиты реализуются в svn штатными средствами: создаем ветку, коммитим туда, не забывая постоянно синхронизировать её с trunk-веткой, потом делаем svn reintegrate (что соответствует git push), после чего ветку удаляем (svn remove). Это то же самое в организационном плане, что и git commit/push, с двумя небольшими отличиями. Первое, эту акцию в svn нужно планировать заранее. Второе — промежуточные коммиты в git остаются на рабочем месте (в локальном репозитории), а в svn попадают в удалённый репозиторий. Именно поэтому использование git (именно git, как Вы это описали — поработали немного, сделали несколько коммитов, дошли до какой-то логической точки и запушили) у нас в фирме запрещено. И на это есть веская причина — если разработчик сделал 99% задачи, но не сделал ни одного git push, это все остаётся на его рабочем месте. Если с рабочим компом что-то случится, результаты его работы потеряны навсегда. Поэтому только svn, но в режиме множественных короткоживущих веток и svn reintegrate. Так что от svn я не отвык и еще лет 10 минимум не отвыкну.


              Во-вторых, Вы упустили из виду очень полезный совет — «не плоди сущностей без надобности». Вы на ровном месте ввели совершенно ненужную сущность — микро-релиз. Сейчас поясню. В моем случае, репозиторий завязан на F-Droid, то есть при создании метки F-Droid автоматически собирает и публикует релиз приложения. То есть сущность релиза — это то, что официально опубликовано на F-Droid.


              Что такое автоматическая сборка (snapshot) — тоже всем понятно. Это нечто без версии, но полностью соответствующее в данный момент исходному коду. Эта сущность уже существует, как минимум, на рабочей станции разработчика. Почему бы её просто не опубликовать?


              Snapshot, кстати, хорошо вписывается в agile-модель. Релиз — это результат спринта. Автоматические сборки — отработка стори-поинтов. Стори-поинт готов, закоммичен, тестировщик тестирует только его, и какая-либо искусственная нумерация здесь мало что даёт.


              То есть то, что вы описали как недостаток (сборка хранится в репозитории и отличить одну от другой можно только по хешу коммита), я расцениваю, как достоинство. Конечного пользователя эти сборки не касаются, для него есть официальные релизы. А тестировщик и так знает, что делает. Если начинать нумеровать (myapp_1.1.60.19.apk), то через год таких файлов будут сотни. Что с ними делать? Удалять? До какого времени?


              Ваше предложение, конечно же, будет во многих случаях очень хорошо работать. Но мой метод, мне кажется, просто проще и понятней.

            • 0
              Может кто-то поделиться советом, каким сервисом пользоваться если у тебя Mercurial?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.