Эта статья предназначена для iOS-разработчиков.
Прогрессивная часть девелоперской среды практикует методику непрерывной интеграции (CI), и
Что мы получили в итоге:
- Сервер начинает сборку:
- по
веб-хуку в случае push’а вmaster-ветку ; - по команде в чате slack с указанием нужной ветки и доп. параметров.
- по
- Выполняет Unit и UI-тесты.
- Получает следующие метрики:
- покрытие кода тестами;
- количество строк кода;
- дублирование кода;
- цикломатическая сложность кода.
- Архивирует проект в ipa, далее отправляет его на сервер сборок (собственной разработки) и отправляет в slack ссылку на сборку.
Теперь мы экономим кучу времени и сил, забыв, что такое делать сборку и
Для начала ознакомьтесь со следующими оговорками и поймите, сопоставим ли наш гайд с вашими задачами:
- сервер разворачивался под нужды
iOS-разработки ; - установка большинства вспомогательных программ через homebrew, включая и сам Jenkins (быстрые обновления и удобство использования);
- использование xcodebuilder для всех задач
сборки-тестирования (отказались от использования xctoolиз-за невозможности запуска UI-тестов); - в качестве репозитория у нас используется GitLab;
- для хранения сборок мы используем сервер собственной разработки. Для каждой сборки генерируется уникальный URL, и далее достаточно открыть ссылку с мобильного устройства в браузере и нажать «Установить» — благодаря аккаунту Enterprise любой может установить приложение на телефон.
Из-за специфичности действий, связанных с отправкой файлов на наш сервер, этот этап в статье не описывается; - все наши проекты используют систему управления зависимостями СocoaPods.
Руководство получилось довольно громоздким, и мы решили разбить его на две части. Эта часть посвящена тому, как установить и настроить Jenkins.
Что необходимо:
- Mac c установленным OS X и Xcode (В нашем случае MacBook Pro 2011 года с OS X 10.11.4);
- несколько часов свободного времени.
Создание пользователя Jenkins и его настройка
Создать пользователя Jenkins можно как через консоль, так и используя GUI. Вряд ли во втором варианте возникнут сложности, поэтому мы лучше рассмотрим первый (источник):
#Создание группы 'Applications' dseditgroup -o create -n . -u username -p -r 'Applications' applications #Получение идентификатора для группы sudo dscl . -read /Groups/applications #Получение списка идентификаторов для пользователей (будет нужен уникальный идентификатор для пользователя) sudo dscl . -list /Users UniqueID #Создание пользователя (значения идентификаторов должны быть уникальными) sudo dscl . -create /Users/jenkins sudo dscl . -create /Users/jenkins PrimaryGroupID 777 sudo dscl . -create /Users/jenkins UniqueID 1777 sudo dscl . -create /Users/jenkins UserShell /bin/bash sudo ddcl . -create /Users/jenkins RealName "Jenkins" sudo dscl . -create /Users/jenkins NFSHomeDirectory /Users/jenkins sudo dscl . -passwd /Users/jenkins #Создание домашней директории и установка прав на неё sudo mkdir /Users/jenkins sudo chown -R jenkins /Users/jenkins
Наш пользователь готов, и теперь нам нужно зайти за него. Мы можем произвести вход в систему через GUI, либо зайти, используя консоль:
sudo -u jenkins -i
Внимание: все дальнейшие действия мы совершаем под пользователем Jenkins.
Установка необходимых программ
Для установки Jenkins воспользуемся системой управления пакетами Homebrew. В дальнейшем это также упростит процесс установки и обновления дополнительных пакетов, которые мы будем использовать для получения метрик кода.
1. Установка Homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2. Установка jenkins:
brew install jenkins
Установка системы управления зависимостями cocoapods:
sudo gem install -n /usr/local/bin cocoapods
Чтобы наш сервер автоматически запускался при старте системы, нам необходимо настроить запуск соответствующей задачи для launchd. У нас есть выбор: сделать это через LaunchAgents или LaunchDaemon. Мы воспользуемся именно LaunchAgents,
Daemon | Agent | |
---|---|---|
Launch Time | System start | User login |
User Type | Login | |
Home Folder | No | Yes |
Login Keychain | No | Yes |
iOS Simulator | No | Yes |
Provisioning Profiles | No | Yes |
*Главная проблема запуска Jenkins через Daemon заключается в том, что нельзя выполнить тестирование, не запустив iOS Simulator (подробнее про разницу использования демона и агента можно прочитать здесь)
Однако
Для настройки запуска через LaunchAgents выполним следующие шаги:
1. выгрузим демона (был создан автоматически при установки Jenkins):
sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.jenkins.plist
2. удалим демона:
sudo rm /Library/LaunchDaemons/homebrew.mxcl.jenkins.plist
3. создадим агента:
cd /Users/jenkins/Library/LaunchAgents tap homebrew.mxcl.jenkins.plist
4. настроим агента, используя
vim homebrew.mxcl.jenkins.plist
Пример содержимого файла plist:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>homebrew.mxcl.jenkins</string> <key>ProgramArguments</key> <array> <string>/usr/bin/java</string> <string>-Dmail.smtp.starttls.enable=true</string> <string>-jar</string> <string>/usr/local/opt/jenkins/libexec/jenkins.war</string> <string>—httpListenAddress=<nobr>0.0.0.0</nobr></string> <string>--httpPort=8080</string> </array> <key>RunAtLoad</key> <true/> <key>UserName</key> <string>jenkins</string> </dict> </plist>
Здесь стоит обратить внимание на поле httpListenAddress со значением
Напомню: для закрытия и сохранения файла в редакторе vim вводим:wq
После установки Jenkins доступен по умолчанию по адресу
Для доступа из внешней сети можно пробросить порты на маршрутизаторе: 8080 для
Установка плагинов для Jenkins
Заходим на сервер Jenkins -> Настроить Jenkins -> Управление плагинами. Во вкладке «Доступные» находим и устанавливаем следующие плагины:
Role-based Authorization Strategy — обеспечение безопасности. Даёт возможность создания групп пользователей с распределением прав;- GitLab Plugin и Gitlab Hook Plugin- плагины для работы с gitlab;
- Xcode integration — интеграция с Xcode;
- Keychains and Provisioning Profiles Management — облегчает работу с provisioning profile.
Базовая настройка Jenkins
Через
1. Настройки Xcode Builder. Если вы устанавливали Xcode в стандартную директорию, то настройки менять не нужно. В противном же случае необходимо задать путь для указанных компонентов. Учитывая, что мы запустили сервер с использованием LaunchAgents, то Xcode будет иметь доступ к login.keychain. Но если вы хотите подстраховаться, то можете в этом разделе добавить keychain, как указано на скриншоте (здесь и далее нажмите на изображение для просмотра).
Теперь мы можем загрузить необходимые сертификаты через GUI или через shell в login.keychain, а Xcode будет автоматически подтягивать нужные ему во время сборки.
2. Для настройки доступа по ssh делаем так:
a. генерируем
b. добавляем ключ в разделе CVS;
c. для доступа к GitLab перейдём в раздел Credentials на главной странице
d. в настройках самого GitLab необходимо указать соответствующий открытый ключ:
3. Для настройки безопасности воспользуемся плагином
Создание job’а и настройка сборки проекта
1. Чтобы создать Job в Jenkins на главной странице веб-интерфейса выбираем «Создать Item». Далее выбираем пункт «Создать задачу со свободной конфигурацией» и вводим название проекта;
2. на странице настройки «джобы» Jenkins первое, что мы сделаем — это перейдём во вкладку «Управление исходным кодом» и настроим загрузку проекта с GitLab. Здесь нам нужно ввести адрес репозитория нашего проекта, указать, с помощью каких полномочий (credentials) сервер получит доступ к GitLab и какую ветку проекта необходимо собрать:
3. Далее переходим в раздел «Сборка». Если вы используете систему управления зависимостями CocoaPods и не добавляете Pod файлы в git, нужно добавить шаг сборки «Выполнить команду shell», в которой запустить установку подов:
#!/bin/bash -l export LANG=UTF-8 pod install
Тут можно обернуть установку условием, чтобы не устанавливать поды каждый раз. Пример (если файл обновился за последние 60 секунд, то выполнить…):
if [ $(( $(date +"%s") - $(stat -f %m Podfile) )) -le 60 ]; then pod install fi
4. Добавляем шаг сборки: Xcode. Если вы используете систему управления зависимостями CocoaPods, то вам не следует указывать значение в поле Target, а в разделе Advanced Xcode build options указать название исполняемой схемы и файла.xcworkspace. Пример минимальной конфигурации показан на скриншотах ниже:
Стоит отметить, что в настройках вашего проекта исполняемая схема должна быть отмечена как shared (контейнер может быть как workspace, так и project):
Работа с сертификатами и provisioning profile
Благодаря плагину Keychains and Provisioning Profiles Management можно значительно упростить работу с установкой provisioning profile.
Добавление provisioning profile:
- переходим в настройки Jenkins;
- в списке выбираем Keychains and Provisioning Profiles Management;
- нажимаем кнопку «Выберите файл», находим свой provisioning profile и нажимаем Upload.
Чтобы указать конкретный Provisioning Profile, если это необходимо, то в настройках job’a в разделе «Cреда сборки» нужно установить флаг Mobile Provisioning Profiles и выбрать один из загруженных профайлов:
Затем в настройках Xcode нужно установить custom xcodebuild arguments, как на скриншоте:
Для загрузки сертификата в связку ключей login.keychain можно воспользоваться GUI (просто кликнуть по сертификату), но такая возможность есть не всегда. Поэтому мы рассмотрим чуть более сложный вариант — добавление через ssh:
- скачиваем нужный сертификат на свой Mac и устанавливаем в локальный keychain access двойным кликом;
- заходим в keychain access, находим нужный сертификат и экспортируем ключ в формат.p12;
- передаём сертификат на сервер:
scp certificate.crt jenkins@server:/Users/jenkins
certificate — название сертификата;
jenkins — имя пользователя;
server — адрес сервера;
:/Users/jenkins — путь сохраняемого файла;
(Можно использовать параметр для указания нужного порта в формате: scp -P 20 certificate.crt jenkins@server:/Users/jenkins)
4. передаём ключ на сервер:
scp privatekey.p12 jenkins@server:/Users/jenkins
5. подключаемся к серверу по ssh:
ssh jenkins@server
6. открываем доступ к связке ключей:
security unlock-keychain -p password /Users/jenkins/Library/Keychains/login.keychain
7. устанавливаем сертификат:
security add-certificates ./certificate.crt
8. копируем ключ:
security import privatekey.p12 -k /Users/jenkins/Library/Keychains/login.keychain -P password -A
Подробнее о команде security можно прочитать здесь.
Более подробную информации о том, как добавить конкретный сертификат в job, можно найти в документации соответствующего плагина.
Перед запуском
Теперь мы готовы нажать на кнопку Built Now на главной странице
Если вы всё сделали правильно, то по окончанию сборки в конце лога будет указано: Finished: SUCCESS, а на главной странице Jenkins рядом с названием сборки будет гореть синий индикатор УСПЕХА.
Установка всех необходимых программ и плагинов.
Для начала установим программы, которые будут собирать для нас статистику:
#Определение степени покрытие кода тестами brew install gcovr #Счётчик строк кода brew install cloc #Счётчик строк кода, альтернативный вариант brew install sloccount #Поиск дублирования кода brew install pmd #Генерация отчётов о результатах тестов (также генерирует данные для oclint) sudo gem install xcpretty #Статический анализ кода brew tap oclint/formulae rew install oclint
Далее нам нужно установить плагины для Jenkins, которые будут отображать полученную статистику в удобочитаемом виде:
- PMD
Plug-in — генерация отчёта по статистической сложности кода; - SLOCCount
Plug-in — генерация отчёта по количеству строк кода; - Test Results Analyzer Plugin — генерация отчёта по результатам тестов;
- Cobertura Plugin — генерация отчета по покрытию кода тестами;
- DRY
Plug-in — генерация отчёта по дублированию кода.
Также установим вспомогательные плагины:
- Environment Injector Plugin — внедрение переменных в проект;
- Pre SCM BuildStep Plugin — внедрение переменных до начала выполнения job’а;
- Build Authorization Token Root Plugin — запуск job’а по
get-запросу с токеном; - Parameterized Trigger plugin — позволяет запускать job’ы с параметрами по окончанию сборки;
- Slack Notification Plugin — отправка сообщений в командный чат Slack;
- Publish Over SSH — этот плагин указан здесь в качестве примера. Он подойдет вам, если вы, как и мы, отправляете данные через SFTP на сервер.
Интеграция с чатом Slack
Для получения уведомлений о состоянии сборки в командном чате Slack, нам необходимо добавить соответствующую интеграцию с Jenkins в настройках Slack’а. Это можно сделать здесь.
После создания интеграции будет сгенерирован уникальный токен, который необходимо добавить в настройки Jenkins’а (либо в настройки отдельного job’а) — как в примере на скриншоте:
Далее настроим запуск сборок с помощью встроенного механизма команд в Slack’е. Для начала нам необходимо добавить интеграцию. Для этого пройём в подраздел Slash commands в разделе Custom Integrations и нажмём на кнопку Add configurations. Эту операцию можно выполнить здесь.
При настройке вам нужно указать название вашей команды, выбрать метод передачи данных POST и указать URL-адрес, на который будет идти запрос.
Рассмотрим пример формирования URL для запроса подробнее. Наш URL для примера выглядит так:http://server:8080/buildByToken/buildWithParameters?job=JenkinsExecutor&token=XXXXXXXXXXXXXXXXX
Разберём его по составляющим:
- server — это внешний адрес вашего сервера. Если необходимо, то здесь также указываем нужный порт (в нашем случае 8080);
- buildByToken — возможность, предоставляемая плагином Build Authorization Token Root Plugin. Позволяет запускать job Jenkins по ссылке с указанием токена (в нашем случае XXXXXXXXXXXXXXXXX);
- buildWithParameters — указывает на то, что нужно запустить параметризованную сборку;
- JenkinsExecutor — название job’а, который мы создадим и будем использовать для запуска других job’ов. О нём речь пойдет ниже;
- XXXXXXXXXXXXXXXXX — значение токена, который устанавливается в настройках плагина в конфигурации каждого отдельного job’а.
В качестве примера будем использовать следующую структуру команды:
/build Example test master
- /build — название нашей команды;
- Example — название job’a;
- test — вспомогательный флаг, связанный с запуском тестов и созданием отчётов с метриками;
- master — ветка для сборки.
Рассмотренная конфигурация позволит нам запускать сборку любого проекта с указанием нужной ветки, при этом будет использоваться единая команда: /build.
Конфигурация вспомогательного job’a — JenkinsExecutor
Данный job будет нужен нам для того, чтобы запускать другие job’ы. В нём также можно будет обрабатывать ошибки, если пользователь ввёл не существующий проект, и добавить информацию о команде (своеобразный help).
Заходим на сервер и создаём новую задачу со свободной конфигурацией и названием JenkinsExecutor. Далее в настройках job’а выставляем флаг, указывающий на то, что сборка является параметризированной и принимает параметр text. При запуске команды в Slack’е все данные (Example master test) будут передаваться единой строкой в переменной text.
Далее устанавливаем флаг, отвечающий за запуск сборки удалённо. Здесь нужно указать токен, идентичный тому, который мы установили в настройках команды в Slack’е:
Теперь нам необходимо извлечь значения из переменной text. Для этого переходим в раздел «Сборка» и добавляем шаг сборки «выполнить команду shell». Пример команды:
#Создаём массив из элементов строки, разделённых пробелом IFS=' ' read -a array <<< "$text" #Согласно нашему примеру, первое значение — это название проекта JOB_NAME=${array[0]} #Флаг, ответственный за тесты TEST=${array[1]} #Название ветки проекта BRANCH=${array[2]} #Если необходимо, можно также получить другие значения: USER_NAME=${user_name} CHANNEL_NAME=${channel_name}
Для запуска сборки с параметрами отправим POST-запрос на исполнение конкретного job’а. Для этого к предыдущей
curl -d TEST=${TEST} -d BRANCH=${BRANCH} -X POST \ -u username:password http://<nobr>127.0.0.1</nobr>:8080/job/${JOB_NAME}/buildWithParameters
Здесь password — это API key пользователя username (пользователь должен иметь права на запуск job’ов).
Чтобы получить ключ:
- Нажмите на username в правом верхнем углу
веб-интерфейса Jenkins; - Нажмите на кнопку «Настроить» в левой части экрана;
- Нажмите на Show API Key — искомый ключ у нас.
Обратите внимание, что все запускаемые сборки должны быть параметризированными!
Настройка сборки
1. Первое, на что стоит обратить внимание при настройки job’а — это то, что сборка должна быть параметризированной. Для этого выставляем соответствующий флаг, добавляем текстовые параметры BRANCH и TEST и задаём им параметры по умолчанию:
Здесь стоит отметить, что для переменной BRANCH нужно дополнительно добавить значение по умолчанию. Дело в том, что если вы запустите сборку из Slack без указания ветки, то в переменной BRANCH будет пустое значение и соответственно будет ошибка. Для этого мы добавим флаг Run buildstep before SCM runs в разделе «Среда сборки». Затем добавим шаг «выполнить команду shell» и шаг inject environment variables. Делаем по примеру:
2. Настраиваем взаимодействие с GitLab.
Указываем адрес репозитория проекта. Указываем ветку сборки (в нашем случае это переменная BRANCH).
3. Настраиваем сборку по
В триггерах сборки устанавливаем флаг «Сборка по пушу в GitLab». Добавляем нужные параметры и указываем ветку, для которой будет срабатывать триггер:
Затем в настройках проекта на GitLab в категории Web hooks добавляем
4. Этап сборки начинается с выполнения
if [ $(( $(date +"%s") - $(stat -f %m Podfile) )) -le 60 ]; then pod install fi
Затем для удобства установим некоторые переменные для проекта и запишем их в файл:
#Название .ipa-файла PROJECT_NAME="Example" #Название файла .xcworkspace WORKSPACE_NAME="Example" #Название исполняемой схемы SCHEME_NAME="Example" #Название папки с исходниками. Будет использоваться для подсчёта количества строк кода FOLDER_NAME_WITH_CODE="Example" #Записываем переменные в файл, чтобы использовать в других этапах сборки echo PROJECT_NAME=$PROJECT_NAME > build.properties echo WORKSPACE_NAME=$WORKSPACE_NAME >> build.properties echo SCHEME_NAME=$SCHEME_NAME >> build.properties
В зависимости от установленного параметра TEST запускаем или пропускаем этап тестирования и генерацию отчётов. Пример того, как это может выглядеть:
if [ "$TEST" == "test" ]; then #Создание папки reports, в которую мы будем складывать отчёты if [ ! -d "reports" ]; then mkdir "reports" fi #Тестирование и создание отчётов для анализа xcodebuild -workspace ${WORKSPACE_NAME}.xcworkspace \ -scheme ${SCHEME_NAME} \ -configuration Debug \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 6' \ -IDECustomDerivedDataLocation="build_ccov" \ GCC_GENERATE_TEST_COVERAGE_FILES=YES \ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES \ clean test | xcpretty -r junit -o reports/junit.xml -r json-compilation-database -o compile_commands.json #Publish JUNIT test = **/reports/junit.xml #Анализ синтаксической сложности кода oclint-json-compilation-database -v -e Pods -- \ -rc=LONG_LINE=200 \ -rc=NCSS_METHOD=60 \ -rc=LONG_METHOD=100 \ -rc=MINIMUM_CASES_IN_SWITCH=1 \ -report-type pmd \ -o reports/oclint.xml \ -max-priority-1 1000 \ -max-priority-2 1000 \ -max-priority-3 1000 #Publish PMD analysis = **/reports/oclint.xml #Анализ покрытия кода тестами gcovr --object-directory="build_ccov/${SCHEME_NAME}/Build/Intermediates/${SCHEME_NAME}.build/"\ "Debug-iphonesimulator/${SCHEME_NAME}.build/Objects-normal/x86_64/" \ --xml \ --print-summary \ --exclude '.*Tests.*' \ --exclude '.*Libs.*' \ --exclude '.*ExternalFrameworks.*' \ --exclude '.*Platforms.*' \ --output=reports/coverage.xml #Publish Cobertura Coverage = **/reports/coverage.xml #Подсчёт строк кода (два варианта): cloc ${WORKSPACE}/${FOLDER_NAME_WITH_CODE} -by-file -skip-uniqueness -xml -out=${WORKSPACE}/reports/cloc.xml #Publish SLOCCount analysis = **/reports/cloc.xml sloccount --duplicates --wide --details ${WORKSPACE}/${FOLDER_NAME_WITH_CODE} -v > reports/sloccount.sc #Publish SLOCCount analysis = **/reports/sloccount.sc #Анализ дублирования кода pmd cpd --files ${WORKSPACE}/${FOLDER_NAME_WITH_CODE} \ --minimum-tokens 10 --language objectivec \ --encoding UTF-8 \ --format net.sourceforge.pmd.cpd.XMLRenderer | iconv -f macRoman -t utf-8 | sed 's/MacRoman/UTF-8/g' > reports/duplicated-code.xml #Publish duplicate code = **/reports/duplicated-code.xml else touch reports/junit.xml #Данная строчка нужна, чтобы избежать провала при сборке из-за генерации отчета плагином Publish JUNIT test result report fi
Подробную информацию по синтаксису команд ищите на соответствующих страницах документации:
5. Записанные ранее переменные необходимо внедрить в процесс сборки. Для этого добавляем шаг сборки Inject environment variables и указываем нужный путь:
6. Следующий этап — создание сборки и архивирование в.
7. Последний шаг — добавить послесборочные операции.
Мы сгенерировали файлы для пяти отчетов, и теперь нам нужно передать эти файлы соответствующим плагинам:
- Publish PMD analysis results = **/reports/oclint.xml
- Publish duplicate code analysis results = **/reports/
duplicated-code .xml - Publish Cobertura Coverage analysis results = **/reports/coverage.xml
- Publish SLOCCount analysis results = в зависимости от используемого модуля:
- **/reports/cloc.xml
- **/reports/sloccount.sc
- Publish JUNIT test result report = **/reports/junit.xml (Примечание: В расширенных настройках плагина нужно установить флаг Do not fail the build on empty test results. Это поможет избежать
fail-статуса для сборки, если она была запущена без запуска тестов)
На этом этапе мы можем отправить полученный в случае успеха.
Последний шаг, который нам нужно добавить — Slack Notification, который, как вы догадались, отправляет уведомления в Slack. В расширенных настройках плагина можно указать индивидуальные настройки для текущего job’а. Стоит заметить, что в качестве сообщения можно указать переменную (пример: $MESSAGE), значение которой менять на разных этапах сборки и тем самым отправлять более информативные сообщения в Slack.
На этом внедрение CI можно считать оконченным. Мы надеемся, что наша статья будет для вас полезной.