Со своим первым сетапом я промучился около 3-х недель! Не повторяйте моих ошибок!

На дворе 2023 год, и вот вы и ваша команда наконец решили отказаться от CI-пайплайна, которым вы пользовались, в пользу автономного Jenkins CI. Замечательно! В этой статье мы не будем разглагольствовать о плюсах и минусах использования одних CI-систем в сравнении с другими, а сразу сосредоточимся на том, как настроить полностью функциональную среду Jenkins CI для iOS.

Хоть вы, вероятно, и сможете самостоятельно настроить и запустить среду Jenkins всего за пару часов, и, возможно, у вас, скорее всего, получится запускать в ней сборку для iOS, в этом деле существует множество мелких подводных камней, которые со временем усугубляются и делают быструю незатейливую реализацию довольно нестабильной.

Лично мне пришлось потратить несколько недель на эксперименты в попытках настроить все правильно, чтобы больше не сталкиваться с целым рядом загадочных ошибок и “время от времени” возникающих проблем. Что удивительно, я не нашел никакого исчерпывающего руководства по “лучшим практикам Jenkins”, поэтому решил написать собственное, в котором я поделюсь с вами знаниями, усвоенными на собственном горьком опыте.

Предварительные требования

Описывать шаги, необходимые для получения машины с macOS, я не буду. В этой статье предполагается, что у вас уже есть какая-нибудь машина под macOS (да, без macOS никак), и вы уже установили Jenkins на своем сервере. Если Jenkins еще не установлен, то вам поможет это официальное руководство (этот шаг достаточно прост):

https://www.jenkins.io/doc/book/installing/macos

Кроме того, в этой статье мы будем использовать Homebrew, rbenv, xcodes, и Bundler. Я не буду вдаваться в подробности, почему я выбрал именно эти программные решения (возможно, в следующем посте ????), но не стесняйтесь спрашивать меня, если вам очень интересно!

Установка зависимостей

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

Обновление вашего ~/.zshrc файла

Следующие настройки должны облегчить вам запуск и работу, а также обеспечить необходимые меры безопасности для fastlane. Добавьте это в свой файл ~/.zshrc:

# Инициализируем rbenv, если он уже установлен.
export PATH=$PATH:/usr/local/bin:$HOME/.rbenv/bin:$HOME/.rbenv/shims
if which rbenv > /dev/null; then
  eval "$(rbenv init -)"
fi

# Эти версии вы должны установить сами в соответствии с потребностями/конфигурацией вашего проекта.
export XCODE_VERSION="14.3"
export BUNDLER_VERSION="2.2.32"
export RUBY_VERSION="3.1.2"

# В целях безопасности попросите Fastlane не сохранять ваш пароль.
# Несмотря на то, что ваши рабочие процессы и скрипты CI, скорее все, уже не используют App Store Connect User + Password, если вам вдруг случится запускать fastlane вручную на машине, выделенной под CI-среду, вы бы не хотели, чтобы она случайно сохранила ваш пароль Apple ID в свой Keychain.
export FASTLANE_DONT_STORE_PASSWORD="1"

# Требуется для fastlane (во избежание проблем с юникодом)
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8

Следует отметить, что в зависимости от используемого вами поставщика облачных услуг (например, AWS, Azure и т.д.) файл ~/.zshrc может уже содержать что-нибудь. В таком случае, чтобы избежать проблем, просто добавьте приведенный выше фрагмент в конец файла.

После обновления вашего ~/.zshrc файл, подгрузите его, чтобы применить изменения (или просто завершите текущий SSH-сеанс и запустите новый):

source ~/.zshrc

Затем, чтобы упростить себе настройку зависимостей, вы можете скопировать в свой терминал следующий фрагмент

echo "Installing rbenv and the right ruby version that your project uses"
brew install rbenv ruby-build
rbenv install $RUBY_VERSION
rbenv global $RUBY_VERSION

echo "Speeding up gem installs"
echo "gem: --no-document" >> ~/.gemrc

echo "Initializing rbenv (will run the initialization code that we just saved in the ~/.zshrc file)"
source ~/.zshrc

echo "Installing bundler"
gem install rubygems-update
gem update --system
gem install bundler -v $BUNDLER_VERSION

echo "Installing xcodes"
brew install xcodesorg/made/xcodes

echo "Installing the Xcode version your team uses"
echo "Note that this one is gonna take a long while (maybe 10-20 minutes). Take a break, and once it's, done you're gonna need to enter the sudo password so the installation completes"
brew install aria2
xcodes install $XCODE_VERSION --experimental-unxip --select --update

Учетные данные git

Я написал целую статью, посвященную этому шагу. На этом этапе мы прервемся, пока вы не разберетесь с учетными данными git на вашей машине с Jenkins, следуя инструкциям из статьи, приведенной ниже:

Аутентификация с помощью GitHub в Jenkins CI через GitHub App

Настройка джобов

Из-з того, как Jenkins выбирает, какие ветки билдить, при использовании различных стратегий для “обнаружения” ветвей (т.е. “Исключить ветки, которые также помечены как PR” или “Только ветки, которые также помечены как PR”) возникает своего рода состояние гонки. Это приводит к таким проблемам, как застревание PR (пулл-реквестов) в состоянии постоянного ожидания (“pending”), что блокирует их мерж. По этой причине нам понадобятся 2 конвейера:

  • Один будет билдить все ветки, которые не должны быть отмечены как PR, например. main, master, development, staging, production (в зависимости от того, как вы их называете).

  • Второй будет билдить все остальные ветки, которые могут стать PR.

Мне потребовалось очень много времени, чтобы понять это, поскольку примерно в 5-10% случаев PR застревали и не могли быть замержены, потому что попадали в какое-то странное пограничное состояние. Это было единственное полностью рабочее решение, которое я нашел для этой проблемы.

Настройка джобы, которая билдит все ветки, которые не станут пулл-реквестами

Чтобы создать новую джобу перейдите на https://<your_jenkins_domain.com>/view/all/newJob. Из доступных вариантов выберите Multibranch Pipeline:

Мне нравится добавлять тип проекта (в данном случае Multibranch Pipeline) к имени конвейера, чтобы я сразу мог понять, какова его структура, на основе одного его имени. Таким образом, в данном случае я бы назвал его как-то вроде protected-branch-multibranch-pipeline ????.

В рамках подготовки этой джобы вам необходимо уделить внимание следующим ключевым параметрам:

  • Учетные данные GitHub: выберите учетные данные приложения GitHub (GitHub App Credentials), которые вы добавили в разделе “Учетные данные git” выше.

  • Добавить новое правило “Discover branches” и выбрать “All branches”.

    • Добавьте в этом разделе фильтр “Filter by name (with regular expression)”, с текстом “master” или “(master|staging|production)”

  • Периодический запуск: ☑️

    • Интервал: 1 минута.

    • Честно говоря, это пережиток того времени, когда Jenkins не всегда обнаруживал сборки, которые было необходимо запустить. Но это было в самом начале моего приключения, еще до того, как я начал использовать GitHub App, поэтому я не до конца уверен, что от этой настройки можно отказаться. Может и можно.

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

Настройка джобы, которая будет билдить пулл-реквесты

Посетите https://<your_jenkins_domain.com>/view/all/newJob еще раз, чтобы создать новую джобу, и снова выберите Multibranch Pipeline из доступных вариантов. Опять же, я рекомендую называть его как-то похоже на pull-requests-multibranch-pipeline, чтобы вы могли легко идентифицировать его в будущем.

Выполните те же шаги, что и выше, за исключением того, что правило поведения, которое вы собираетесь добавить, будет “Discover pull requests from origin”, выбрав стратегию “The current pull request revision“:

Настройка вашего Jenkinsfile

Вы же не думали, что я пропущу самую важную часть?

В приведенных выше шагах при настройке новых джобов вам нужно было указать путь к вашему Jenkinsfile. Способ настройки Jenkins, описанный в этой статье, должен соответствовать определенному способу настройки Jenkinsfile и CI-скриптов, о чем я расскажу вам ниже.

Вот шаблон, который вы можете использовать для основного Jenkinsfile, который можно использовать для запуска обоих protected-branch-multibranch-pipeline и pull-requests-multibranch-pipeline.

pipeline {
    agent any
    options {
        ansiColor('xterm') // Добавляем цвет в логи, активируется через https://github.com/jenkinsci/ansicolor-plugin
        timeout(time: 8, unit: 'HOURS') // Устанавливаем ограничение времени ожидания окончания сборок
        disableConcurrentBuilds(abortPrevious: true) // Отменяем предыдущую сборку при поступлении новых коммитов в ту же ветку
    }
    environment {
        // Здесь настраиваем все наши секреты (также известные как учетные данные), например, ключи API для fastlane, danger и т.д.
        APP_STORE_CONNECT_API_KEY_ISSUER_ID = credentials('APP_STORE_CONNECT_API_KEY_ISSUER_ID')
        APP_STORE_CONNECT_API_KEY_KEY = credentials('APP_STORE_CONNECT_API_KEY_KEY')
        APP_STORE_CONNECT_API_KEY_KEY_ID = credentials('APP_STORE_CONNECT_API_KEY_KEY_ID')
        DANGER_GITHUB_API_TOKEN = credentials('DANGER_GITHUB_API_TOKEN')
        MATCH_PASSWORD = credentials('MATCH_PASSWORD')
        // …и т.д.
    }
    stages {
        stage("1. Set Up") {
            steps {
                withCredentials([usernamePassword(credentialsId: '<team_name>_github_app', usernameVariable: 'GITHUB_APP_USERNAME_TOKEN', passwordVariable: 'GITHUB_APP_PASSWORD_TOKEN')]) {
                    sh '''
                    source ~/.zshrc # Необходимо запустить, чтобы настроить правильную переменную окружения PATH, инициализировать rbenv и все остальное, что мы настроили ранее в файле ~/.zshrc

                    # Ссылки: https://git-scm.com/docs/gitcredentials#_custom_helpers и https://stackoverflow.com/q/61146986/4075379
                    git config credential.username ${GITHUB_APP_USERNAME_TOKEN}
                    git config credential.helper "!echo password=${GITHUB_APP_PASSWORD_TOKEN}; echo"

                    # С этого момента вы можете добавлять сюда свои скрипты, например, make, bundle install, pod install, xcodebuild build, test и т.д.
                    make
                    '''
                }
            }
        }
        stage("2. Static Code Analysis") {
            steps {
                sh '''
                source ~/.zshrc # Да, к сожалению, вам нужно запускать это каждый раз, когда вы объявляете новый "sh" shell-скрипт в ваш Jenkinsfile.
                bundle exec rake danger
                '''
            }
        }
        stage("3. Build & Distribute") {
            steps {
                withCredentials([usernamePassword(credentialsId: '<team_name>_github_app', usernameVariable: 'GITHUB_APP_USERNAME_TOKEN', passwordVariable: 'GITHUB_APP_PASSWORD_TOKEN')]) {
                    sh '''
                    source ~/.zshrc
                    bundle exec fastlane archive_and_distribute # Это действие требует прямого доступа к переменным среды GITHUB_APP_USERNAME_TOKEN и GITHUB_APP_PASSWORD_TOKEN
                    '''
                }
            }
        }
    }
    post {
        // Всегда очищайте рабочее пространство после того, как закончите его использовать, иначе после нескольких сборок вы забьете диск, и ваша машина, скорее всего, заглохнет.
        always {
            cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, deleteDirs: true)
        }
    }
}

Пара слов о функции “withCredentials”

Функция “withCredentials” работает следующим образом:

// Первый параметр — это имя вашего ID учетных данных GitHub App, как вы его зарегистрировали в Jenkins.
// Второй и третий параметры — это имена переменных, которые вы объявляете сейчас, поэтому вы можете называть их как угодно, но вам нужно будет сослаться на них позже, поэтому постарайтесь сделайте их более-менее связанными.
withCredentials([usernamePassword(
    credentialsId: '<team_name>_github_app',
    usernameVariable: 'GITHUB_APP_USERNAME_TOKEN',
    passwordVariable: 'GITHUB_APP_PASSWORD_TOKEN'
)])

В качестве входных данных здесь выступает Credentials ID, который вы зарегистрировали ранее в своих учетных данных Jenkins. Это дает плагину Jenkins возможность генерировать эфемерные токены (имя пользователя и пароль), которые затем можно использовать для отправки HTTPS-запросов на GitHub. Для вас критически важно понимать, что это HTTPS-аутентификация, а не SSH, поэтому, если у вас есть команды git, использующие SSH в конвейере, вы должны определять, что вы работаете в CI-среде, и переключить их на использование HTTPS. Типичным примером этого в iOS-среде является URL Git, используемый match fastlane.

В первый раз, когда мы генерируем такие эфемерные учетные данные, мы предоставляем их в git config credential.helper, чтобы любая операция git (использующая HTTPS) с этого момента (даже на других этапах) смогла выполняться без повторного запроса имени пользователя и пароля. В приведенном выше примере вы можете наблюдать это на втором этапе, где мы вызываем danger (который публикует комментарии на GitHub, поэтому ему нужны учетные данные) — нам не нужно было генерировать для него новые учетные данные. Но в третьем этапе, где нам нужен прямой доступ к переменным окружения учетных данных (а переменные окружения не сохраняются между этапами), мы повторно генерируем учетные данные и таким образом снова устанавливаем значения для переменных окружения.

Если вам интересно, вот как вы можете настроить экшн match в вашем Fastfile:

git_url = is_ci? ? 
"https://#{ENV["GITHUB_APP_USERNAME_TOKEN"]}:#{ENV["GITHUB_APP_PASSWORD_TOKEN"
]}@github.com/myorg/myrepo.git" : "git@github.com:myorg/myrepo.git"

match(git_url: gir_url)

Примечание 1: вы должны иметь плагин GitHub Branch Source версии 2.7.1 или выше, чтобы использовать эти API. Эта функция представлена и введена в 2020 году.

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

Последние штрихи

Прежде чем мы закончим с настройкой Jenkins, нам осталось сделать всего две вещи.

Зависающие сборки 

Это может произойти, когда ваш конвейер пытается получить доступ к git внутри конвейера (например, при запуске установки пода или получении SPM, и т.д.). Это может быть вызвано тем, что ваша машина запрашивает аутентификацию по паролю из Keychain, но это не отображается в логах Jenkins. К сожалению, единственное решение, которое помогло мне полностью избавиться от этой проблемы, — это войти в систему с доступом к пользовательскому интерфейсу (например, VNC, а не SSH) и кликнуть “Always Allow" во всплывающем окне, запрашивающем разрешение на доступ к “login.keychain”. Вам нужно будет ввести root-пароль компьютера с macOS при появлении этого запроса.

Но вам достаточно сделать это всего только один раз, и вы больше никогда не вернетесь к этой проблеме.

Учетные данные git Xcode конфликтуют с учетными данными GitHub App

Существует проблема, которая приводит к сбою сборки с ошибкой:

stderr: remote: Invalid username or password

Это происходило примерно в 5-10% сборок, полностью ломая их. Оказывается, учетные данные git Xcode могут конфликтовать с теми, которые мы устанавливаем, и тогда все рушится. Чтобы это исправить, просто следуйте моему ответу на этот вопрос на Stack Overflow:

Jenkins Github Authentication иногда сбоит?

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

Заключение

В этом (объективно) длинном руководстве вам был предложен очень самоуверенный способ настройки вашей машины с Jenkins, адаптированный для iOS-среды. Если вы работаете с другими средами, большинство советов, показанных в этой статье, все-равно пригодятся вам, чтобы избежать тех или иных проблемы, например, с Android, React, Flutter, Node и т.д.  Изменятся, вероятно, только ваши зависимости и некоторые детали в примерах, которые я приводил при настройке Jenkinsfile.

Когда я только настроил свою первую машину с Jenkins, я получил первую сборку за считанные часы, но это среда была еще далека от той, которая соответствовала бы потребностям команды. Например, GitHub Checks определенно необходим (но его не так просто настроить), и среда должна быть на 100% стабильной, лишенной случайных проблем во время сборки, иначе ваша команда не будет доверять этой системе CI. Другими словами, я сделал 80% работы очень быстро, но оставшиеся 20%, чтобы получить отполированный CI-конвейер, заняли у меня буквально недели. Я надеюсь, что следуя этому руководству, вам не придется беспокоиться об окружающей среде, и вы сможете сосредоточиться на том, что действительно важно: на построении идеального пайплайна для вашего проекта и вашей команды ????

В моей следующей статье я расскажу, что вы можете сделать, чтобы сохранить тяжелую работу, которую вы проделали, чтобы настроить свою машину с Jenkins. Проще говоря: как сохранить резервную копию вашего CI! Следите за обновлениями.

Перевод материала подготовлен в рамках набора на специализацию iOS Developer. Узнать подробнее.

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