Автоматизуємо рутинні процеси в iOS-проєктах. Приклади та поради

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Усім, привіт! Я — Богдан, iOS Developer у продуктовій компанії OBRIO з екосистеми Genesis.

З появою нових інструментів для автоматизації розробники можуть спростити чимало рутинних завдань і підвищити ефективність роботи. У цій статті я поділюся практичними прикладами автоматизації деяких процесів в iOS-проєктах, зокрема форматування коду, дистрибуції збірки, запуску релізу та тестування.

Основні інструменти, які ми використовуємо для автоматизації:

  • GitLab — платформа для контролю версій з вбудованим CI/CD функціоналом, яка дозволяє автоматизувати процеси збірки, тестування та розповсюдження застосунків.
  • Fastlane — набір інструментів для автоматизації завдань, пов’язаних з розробкою та релізом застосунків, який дозволяє створювати власні сценарії.
  • SwiftFormat та SwiftLint: інструменти для форматування та лінтингу коду.

Форматування коду з допомогою SwiftLint та SwiftFormat

Якість коду безпосередньо впливає на успіх проєкту. Чистий та добре структурований код полегшує підтримку, масштабування та спільну роботу над ним. Проте забезпечувати єдиний стиль вручну може бути складно та забирати чимало часу. В цьому можуть допомогти інструменти SwiftLint та SwiftFormat:

  • SwiftLint аналізує код, вказуючи на потенційні проблеми та недоліки.
  • SwiftFormat автоматично форматує код, щоби він відповідав встановленому стилю.

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

Водночас якщо запускати SwiftLint та SwiftFormat під час кожної збірки, це значно збільшить час компіляції. Щоби цього уникнути, ми створили скрипт, який запускає ці інструменти лише для файлів, що мають зміни відносно останнього коміту. Це суттєво скоротило час збірки та підвищило продуктивність роботи.

# Перевірка наявності SwiftFormat та запуск для змінених файлів
if which swiftformat >/dev/null; then
  for file in $(git diff --name-only); do
    [[ "${file##*.}" == "swift" ]] && swiftformat "$file"
  done
else
  echo "warning: SwiftFormat not installed"
fi

Автоматизація розповсюдження збірок

16-17 травня, Київ. Квитки тут!👇

Розгортання застосунків для тестування та релізу — трудомісткий процес, який також можна автоматизувати та прискорити.

Firebase

Спершу потрібно оновити дані для релізу. Ми ведемо текстовий файл з нотатками, який містить номер версії та перелік змін. Цей файл автоматично оновлюється після кожного коміту в основну робочу гілку, накопичуючи інформацію про нові функції та виправлення.

desc 'Update release notes'
lane :update_release_notes do
  feature_note_path = 'feature-note.txt'
  release_notes_path = 'release-notes.txt'
  git_pull  # Оновлення останніх змін
  if !File.zero?(feature_note_path)
    File.readlines(feature_note_path).each do |note|
      File.open(release_notes_path, 'a') { |file| file.puts note }
    end
    File.write(feature_note_path, "")  # Очистка файлу нотаток
    git_commit(path: [release_notes_path, feature_note_path], message: "Updated Release Notes")
    push_to_git_remote
  end
end

Далі збільшуємо номер збірки, щоб уникнути конфліктів при розповсюдженні через Firebase. Для девелоперських збірок ми використовуємо версію 100.0, що дозволяє впроваджувати спеціальну логіку для режиму розробки. Перед релізом ми встановлюємо актуальну версію відповідно до поточного релізу.

desc 'Bump build version'
lane :bump_build do
  if git_branch == "dev"
    update_project_version(version_number: "100.0")
  else
    version_number = git_branch.gsub(/[^0-9.]/, "")
    # Логіка розрахунку нового номера збірки
    update_project_version(version_number: version_number, build_number: "calculated_build_number")
 end
  commit_version_bump(xcodeproj: ENV['XC_PROJECT'], message: "Build version bump", include: %w[fastlane/release-notes.txt])
end

Далі ми запускаємо процес збірки та завантаження застосунку до Firebase:

  • створюємо збірку для stage та production середовищ;
  • завантажуємо архів до Firebase, в тому числі stage та production версії застосунку.

Варто зазначити, що для stage збірок та версій 100.0, які є індикатором того, що застосунок є тестовою збіркою, ми пропускаємо завантаження символів Crashlytics, щоб зекономити час.

desc "Bump version and distribute to Firebase"
lane :firebase do |options|
  ensure_git_status_clean
  update_release_notes
  clean_adhoc_provisioning
  bump_build
  firebase_qa
  firebase_prod(without_upload: true)
  push_to_git_remote
  slack(message: "Lane took X minutes Y seconds")
end

desc "Distribute to Firebase with QA scheme"
private_lane :firebase_qa do
  cocoapods(try_repo_update_on_error: true)
  build_app(configuration: "QA", scheme: "Nebula QA", export_method: "ad-hoc")
  firebase_app_distribution(app: "QA_APP", firebase_cli_token: "TOKEN", groups: "qa-team", release_notes_file: "release-notes.txt")
  post_to_slack(scheme: "Nebula QA", destination: "Firebase", build_time: "qa_build_duration", build_size: get_build_size)
 clean_archives
end

desc "Distribute to Firebase with Prod scheme"
private_lane :firebase_prod do |options|
  cocoapods(try_repo_update_on_error: true)
  build_app(configuration: "Release", scheme: "Nebula", export_method: "ad-hoc")
  firebase_app_distribution(app: "PROD_APP", firebase_cli_token: "TOKEN", groups: "qa-team", release_notes_file: "release-notes.txt")
  post_to_slack(scheme: "Nebula", destination: "Firebase", build_time: "prod_build_duration", build_size: get_build_size)
  upload_symbols unless options[:without_upload]
  clean_archives
end

Після успішного розгортання ми автоматично відправляємо повідомлення в Slack з інформацією про нову збірку:

  • номер версії та збірки;
  • час збірки;
  • розмір застосунку;
  • нотатки до релізу.

Це допомагає команді бути в курсі останніх оновлень та швидко реагувати на можливі проблеми.

private_lane :post_to_slack do |options|
  slack(message: "New #{options[:scheme]} submitted to #{options[:destination]}:\nBuild time: ...\nBuild size: ... mb\nRelease notes:\n#{File.read('release-notes.txt')}")
end

GitLab CI/CD

Використовуючи GitLab CI/CD, ми створили хук, який автоматично запускає процес дистрибуції при пуші в основну гілку розробки. Ми виключили технічні коміти, такі як збільшення номера збірки або оновлення нотаток до релізу, оскільки немає потреби розповсюджувати ці зміни. Це дозволяє автоматизувати процес без додаткових зусиль з боку розробників.

build:firebase:
  script:
    - bundle exec fastlane firebase --verbose
  rules:
    - if: $CI_COMMIT_BRANCH == "dev" && $CI_COMMIT_MESSAGE !~ /(version bump|Release Notes)/
      when: always

TestFlight

Процес підготовки схожий на Firebase, але з деякими відмінностями:

  • Для релізних збірок версія встановлюється відповідно до запланованого релізу, щоб забезпечити коректність інформації в App Store.
  • Створюємо релізну збірку за допомогою команди build_app.
  • Використовуємо Fastlane команду deliver для автоматичного завантаження застосунку та всіх необхідних метаданих до App Store Connect. Метадані зберігаються у спеціальній теці в проєкті.
  • Завантаження символів Crashlytics: для релізних збірок це обов’язковий крок, який допомагає відстежувати та аналізувати можливі помилки в продакшн версіях.
desc "Distribute to TestFlight with Prod scheme"
lane :app_store do |options|
  clean_appstore_provisioning
  bump_build
  build_app(configuration: "Release", scheme: "Nebula", export_method: "app-store")
 upload_to_testflight(api_key: app_store_connect_api_key(key_id: "KEY", issuer_id: "ISSUER", key_content: "KEY_CONTENT"), changelog: options[:changelog])
  deliver(skip_binary_upload: true, skip_app_version_update: true)
  upload_symbols
  clean_build_artifacts
  post_to_slack(scheme: "Nebula", destination: "TestFlight", build_time: "compile_time", build_size: get_build_size)
  slack(message: "Lane took X minutes Y seconds")
end

Після завершення процесу розгортання команді приходить сповіщення через Slack. Воно містить детальну інформацію про реліз, час збірки та нотатки до релізу.

Автоматизація релізу за допомогою Fastlane та GitLab CI/CD

Щоби спростити процес підготовки до релізу, ми створили спеціальні сценарії у Fastlane:

  • Create_release — ініціює процес релізу, запускаючи його з пайплайну GitLab. Працює собі таким тригером для того, аби всі відповідні джобси почались на dev та release бранчі.
  • Release — виконує вже всі наступні дії для релізу застосунку, включаючи збірку та розповсюдження.

Процес виглядає так:

1. Створюємо релізну гілку з відповідним номером версії.

desc "Create release branch" 
lane :create_release do
  version = File.open("release-notes.txt").gets.chomp
  sh("git checkout -b release_v#{version}")
  sh("git commit --allow-empty -m 'Initial commit for release branch #{version}'")
  sh("git push --set-upstream origin release_v#{version}")
  sh("git checkout dev")
  bump_release_version
  git_commit(path: ["release-notes.txt"], message: "Bump release version")
  push_to_git_remote
end 

2. Збільшуємо версію та очищаємо файл від старих нотаток, комітимо зміни в гілку розробки, щоби підтримувати її актуальність.

desc 'Bump release version to a next one'
lane :bump_release_version do

  # Get version number from release-notes.txt
  version = File.open("release-notes.txt").gets.chomp

  # Increment version
  major, minor, patch = version.split('.').map(&:to_i)
  minor += 1
  formatted_minor = '%02d' % minor
  new_version = "#{major}.#{formatted_minor}.0"

  # Write new version back to release-notes.txt and clean the rest of the file
  File.open("release-notes.txt", 'w') { |file|
    file.puts(new_version)
    file.puts("# This file is generated automatically.\n#\n# Don't change it manually!")
  }
end

3. На релізній гілці автоматично запускаються lanes для розгортання через Firebase та App Store Connect.

desc "Firebase release distribution"
lane :firebase_release do
  firebase(without_upload: false)
end

desc "Release distribution"
lane :release do
  app_store(changelog: File.read("release-notes.txt"))
  slack(message: "Lane took X minutes Y seconds")
end

У GitLab CI/CD ми налаштували правила запуску, щоби процес релізу можна було запустити одним натисканням кнопки:

1. Член команди запускає джобу create_release в пайплайні.
2. Ініціюється виконання lane create_release, який створює релізну гілку та комітить необхідні зміни.
3. Автоматично запускається пайплайн для релізної гілки, який виконує lane release.

Отже, тепер весь процес зводиться до натискання однієї кнопки в GitLab.

build:create_release:
 environment:
   name: staging
 when: manual
 script: bundle exec fastlane create_release --verbose

build:firebase_release:
 script:
   - bundle exec fastlane firebase_release --verbose
 artifacts:
   paths:
     - fastlane/start_time.txt
   expire_in: 24 hrs
 rules:
   - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release_v)(\d+\.)?(\d+\.)?(\d+)$/ && $CI_COMMIT_MESSAGE !~ /(version bump|Release Notes)/
     when: always
 tags: [obrio-dev]

build:release:
 script:
   - bundle exec fastlane release --verbose
 needs:
   - job: build:firebase_release
     artifacts: true
   - job: test:e2e_test_release
 rules:
   - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release_v)(\d+\.)?(\d+\.)?(\d+)$/ && $CI_COMMIT_MESSAGE !~ /(version bump|Release Notes)/
     when: always
     allow_failure: true
   - if: $CI_COMMIT_BRANCH =~ /^(release_v)(\d+\.)?(\d+\.)?(\d+)$/
     when: manual
     allow_failure: true
 tags: [obrio-dev]

Автоматизація тестування

Ми впровадили run_unit_test, який автоматично запускається для кожного Merge Request, перевіряючи, чи не призводять зміни до збоїв у наявному функціоналі. Завдяки автоматизації разом з CI тепер завжди можна бути впевненим, що ми нічого не зламали новими змінами.

desc "Run the unit tests specified in Nebula Dev scheme"
lane :run_unit_tests do
  scan(output_types: "html", output_directory: "~/Documents/iosTestsResults/#{ENV['CI_JOB_ID']}")
end
unit_tests:
  script:
    - bundle exec fastlane run_unit_tests --verbose
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes: [ "Nebula/Classes/Services/**/*", "Core/Services/**/*", "NebulUnitTests/**/*" ]

Також для забезпечення перевірок найпростіших шляхів, через які проходить користувач у нашому застосунку, ми пишемо Е2Е-тести, які відтворюють відкриття певних екранів, фічей. Оскільки ці тести є досить затратними за часом, ми вирішили автоматизувати запуск у двох випадках:

  • щодня вночі, у визначений час, для перевірки коректності останніх змін;
  • перед релізом, щоби переконатися, що реліз-кандидат працює належним чином.

Для тих, хто був уважним, вище вже було згадано джобу для GitLab e2e_test_release. Саме вона запускає всі Е2Е-тести.

test:e2e_test_release:
  extends: .run_e2e_tests_ios
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release_v)(\d+\.)?(\d+\.)?(\d+)$/ && $CI_COMMIT_MESSAGE !~ /(version bump|Release Notes)/
      when: always
  variables:
    ENV_NAME: dev

Тут все доволі просто: є декілька правил, які спрацьовують під час пушу в релізну гілку коду, виключаючи, як і раніше, коміти, де збільшується версія. Що цікавіше — це сам скрипт, який запускає тести:

.run_e2e_tests_ios:
  environment:
    name: $ENV_NAME
    action: verify
  before_script:
    - bundle install --jobs 4 --retry 3
    - java -version
  script:
    - bundle exec fastlane automation_tests --verbose
    - curl -X POST -H 'Content-type: application/json' --data "{ \"text\": \"IOS automation test finished\", ... }" $AUTOMATION_RESULTS_SLACK_WEBHOOK
  after_script:
    - if [ $CI_JOB_STATUS != 'success' ]; then curl -X POST ... ; fi

Скрипт керує повним циклом тестування та розповсюдження збірок автоматично, починаючи від ініціалізації середовища, встановлення та перезапуску необхідних сервісів, аж до запуску ботів та тестів на реальних пристроях iOS. Він виконує підготовчі кроки (очищення пристроїв, перевстановлення застосунків), запускає автоматизаційні тести (включно з Е2Е-скриптами), конвертує зібрані логи у звіти, пушить ці результати до віддаленого репозиторію (GitLab Pages) та врешті надсилає відповідні сповіщення у Slack.

Завдяки цьому нам не потрібно щоразу виконувати багато рутинних дій вручну — усе виконується за допомогою однієї збірки, а відомості про хід і результати процесу завжди доступні в режимі реального часу. Це забезпечує стабільний, прозорий та масштабований процес CI/CD, даючи можливість швидко аналізувати результати тестів, оновлювати застосунок та оперативно реагувати на зміни в кодовій базі.

Висновок

Автоматизація процесів у розробці iOS-застосунків є потужним інструментом, який допомагає економити час, зменшити кількість помилок та підвищити продуктивність команди. З ними команда може оптимізувати робочі процеси та зосередитися на більш важливих завданнях. Якщо у вас є власний досвід автоматизації або ідеї щодо покращення процесів, діліться ними з колегами та спільнотою в коментарях.

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

просто сборка билда в тф и фб? в другом проекте это не переиспользовать.

Неймовірно цікава та корисна стаття, дякую!!

Підписатись на коментарі