Пару недель назад я выступил с докладом о том, как обрести уверенность в процессе релиза, на Mobile Devops Summit, дистанционном мероприятии, организованном Bitrise. В конце концов, я подумал, что было бы неплохо написать статью с описанием специфики выступления и мотивов, стоящих за ним.

Если вы пропустили выступление и хотели бы его посмотреть, я выложил запись на моём канале Youtube.

Мотивация

Для разработчика iOS есть определённые конвейеры CI (continuous integration — непрерывная интеграция), которые имеют решающее значение для доставки приложения, но из‑за своей природы они не запускаются часто. Отличным примером этого является конвейер релиза, который автоматизирует архивирование приложения, его подписание и отправку в App Store Connect и запускается только тогда, когда приложение готово к релизу.

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

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

Ошибки архивации

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

Давайте рассмотрим модульное приложение для iOS, которое определяет iOS 15 как минимальную версию для развёртывания. Мы хотим представить новую функцию, полностью созданную с помощью SwiftUI, поэтому мы создаём новый модуль Schedule (как Swift package):

Package.swift

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
    name: "Schedule",
    products: [
        .library(
            name: "Schedule",
            targets: ["Schedule"]
        )
    ],
    dependencies: [],
    targets: [
        .target(
            name: "Schedule",
            dependencies: []
        ),
        .testTarget(
            name: "ScheduleTests",
            dependencies: ["Schedule"]
        )
    ]
)

Затем новый модуль импортируется целевым приложением и отображается при необходимости. Приложение можно собрать, тесты проходят нормально, новое view (представление, вью, вьюшка) выглядит великолепно, теперь приложение использует SwiftUI!

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

Смотрим на журнал сборки выше: похоже, что приложение архивируется для armv7. Проблема здесь в том, что SwiftUI недоступен в SDK armv7, из‑за чего компилятор не может найти символы SwiftUI. Посмотрев на некоторые связанные ветки форума разработчиков Apple, пришёл к выводу, что если приложение имеет минимальную версию развертывания iOS 11 или выше, его не следует создавать для этой архитектуры.

Так что же происходит? Что ж, после многократных попыток исправить ошибку выясняется, что даже несмотря на то, что новый пакет импортируется target с минимальной версией развертывания iOS 15, минимальная версия iOS должна быть установлена под platforms (платформами) в Package.swift для того, чтобы ошибка исчезла:

Package.swift

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
    name: "Schedule",
    platforms: [
        .iOS(.v15)
    ],
    products: [
        .library(
            name: "Schedule",
            targets: ["Schedule"]
        )
    ],
    dependencies: [],
    targets: [
        .target(
            name: "Schedule",
            dependencies: []
        ),
        .testTarget(
            name: "ScheduleTests",
            dependencies: ["Schedule"]
        )
    ]
)

Обратите внимание, что эта ошибка характерна для Xcode 13 и не является проблемой для Xcode 14.

Ошибки загрузки

Теперь, когда мы увидели ошибку архивирования, давайте рассмотрим пример ошибки загрузки. Давайте теперь рассмотрим, что новый модуль SwiftUI вместо этого является фреймворком (в проекте Xcode) и используется разными target. Каждый из них встраивает и подписывает фреймворк.

Приложение собирается, запускается и архивируется без проблем, но как только оно загружается в App Store Connect, возникает ошибка:

Встраивая и подписывая несколько раз, мы создаём дубликаты одного и того же пакета, что вызывает ошибку загрузки. Это связано с тем, что не может быть более одного пакета с одним и тем же идентификатором. Исправление простое, но иногда его трудно обнаружить, если вы новичок в модульных кодовых базах. Чтобы обойти эту проблему, инфраструктура должна быть встроена и подписана только один раз на уровне target приложения, а затем любой другой target, который её использует, должен отказаться от её внедрения.

Раннее обнаружение этих ошибок

Несмотря на то, что описанные выше проблемы не требовали значительных изменений кода, их диагностика и расследование, безусловно, требовали времени. И это время, на которое вы откладываете свой релиз. Это может не иметь большого значения, если ваш релиз включает только новые функции и улучшения. Но подумайте о ситуации, когда вы делаете выпуск патча, который исправляет серьёзный сбой. Вы, безусловно, хотите, чтобы ваше приложение стало доступно пользователям как можно быстрее.

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

Идея, стоящая за ними, заключается в том, что вы можете запланировать повторный запуск вашего конвейера в определенную дату и время, используя cron expressions. В следующих разделах я создам Github Action, которое заархивирует приложение, подпишет его для App Store, а затем проверит двоичный файл с помощью App Store Connect с помощью fastlane.

Реализация nightly Github Action

Создание nightly lane

Давайте начнём с просмотра того, как выглядит lane релиза в Fastfile:

Fastfile

lane :release do
  # Let's make some magic happen ?
  gym(
    project: "./NutriFit.xcodeproj",
    clean: true,
    derived_data_path: "./derived-data",
    output_directory: "./build",
    output_name: "NutriFit.ipa",
    scheme: "NutriFit",
    export_options: {
      provisioningProfiles: {
        "dev.polpiella.NutriFit" => "NutriFit App Store",
      }
    }
  )

  deliver(
    ipa: "build/Nutrifit.ipa"
  )
end

Глядя на ошибки, обнаруженные выше, новая nightly lane должна убедиться, что приложение может быть правильно заархивировано и что созданный двоичный файл может быть загружен в App Store Connect. Проблема в том, что если мы дословно продублируем код релиза, он отправит сборку в TestFlight, а это не то, что нам необходимо. Вместо этого мы должны использовать флаг доставки verify_only, чтобы сборка никогда не отправлялась, а только проверялась:

Fastfile

lane :release do
  # Let's make some magic happen ?
  gym(
    project: "./NutriFit.xcodeproj",
    clean: true,
    derived_data_path: "./derived-data",
    output_directory: "./build",
    output_name: "NutriFit.ipa",
    scheme: "NutriFit",
    export_options: {
      provisioningProfiles: {
        "dev.polpiella.NutriFit" => "NutriFit App Store",
      }
    }
  )

  deliver(
    ipa: "build/Nutrifit.ipa",
    verify_only: true
  )
end

Если вы хотите узнать больше о том, как работает флаг verify_only, вы можете взглянуть на оригинальный pull request. Я работал над внесением этого изменения в код, поэтому, если у вас есть дополнительные вопросы, не стесняйтесь, напишите мне сообщение в Twitter.

Создание nightly workflow

Теперь, когда nightly lane реализована, её необходимо запускать в определённое время каждый день. Для этого в каталоге .github/workflows создаётся новый файл Github Actions workflow с именем nightly.yml, который работает в системе с macos‑latest в качестве операционной системы и просто вызывает nightly lane в репозитории. Кроме того, в приведённом ниже действии используется тег schedule с chron expression, чтобы сообщить Github, что это действие должно выполняться каждый день в полночь:

nightly.yml

name: Nightly

on:
  schedule:
    - cron: '0 0 * * *'

jobs:
  nightly:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run nightly lane
      run: bundle exec fastlane nightly

Есть некоторые дополнительные настройки, такие как аутентификация с помощью App Store Connect, которые для простоты проигнорированы в этой статье. Если вам интересно узнать немного больше о том, как обрабатывать аутентификацию с помощью App Store Connect и fastlane, следите за этим блогом, так как я скоро напишу статью на эту тему.

Когда запланированные рабочие процессы сияют

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

Они также отлично подходят для выполнения задач, требующих много времени и ресурсов (таких как сквозные тесты) с минимальными перерывами и затратами. Например, вместо того, чтобы запускать E2E‑тесты при каждой отправке на main, их можно запускать один раз вечером, проверяя все изменения, внесённые за день.

И последнее, но не менее важное: еще один отличный вариант использования запланированных запусков CI — это автоматизация повторяющихся процессов. Например, на работе у нас есть сертификат, который необходимо выпускать каждый месяц, поэтому мы создали Github Action, которое запускается в начале каждого месяца и заменяет текущий сертификат новым.