Автоматизирайте тестването, подписването, нотариалното заверяване и разпространението

Мислите ли да напишете приложение за macOS? Искате ли да го разпространявате извън Mac App Store с помощта на Homebrew? В тази статия описвам как да използвате GitHub и GitHub Actions, за да настроите непрекъсната интеграция, доставка и внедряване за приложение за Mac, подписано с Apple ИД на програмист. Най-хубавото е, че ако приложението е с отворен код, всичко, което описвам, е напълно безплатно за използване! (Ако приложението ви не е с отворен код, но се хоства в частно хранилище на GitHub, пак можете да направите това безплатно, при спазване на „ограничения“.)

Защо това е трудно?

Преди можеше просто да компилирате кода си, да го пакетирате в архив или инсталатор, да го поставите на сайт за изтегляне и да очаквате хората да го изтеглят – вече не. Благодарение на разпространението на зловреден софтуер, Apple добави ограничения към операционната система, за да предотврати стартирането на ненадежден код. Тези ограничения усложняват изграждането и опаковането. Освен това потребителите, които са използвали Homebrew, очакват да могат да инсталират и поддържат актуален софтуер от командния ред. Това очакване усложнява и разпространението.

От 2012 г. операционната система Mac включва Gatekeeper, функция, която изисква приложенията да са подписани с предоставени от Apple криптографски сертификати, за да работят. Най-новата версия на macOS дори започна да изисква приложенията, които се разпространяват извън Mac App Store, да бъдат „нотариално заверени“ от Apple, за да работят, „процес“, който изисква да качите приложението си в Apple преди пускането му и след това изчакайте отговор.

Има много причини да използвате Mac App Store за разпространение – за повечето приложения това вероятно е правилното място. Но ако искате да използвате API, които не са разрешени в Mac App Store, или не искате да се занимавате с процеса на преглед, тогава може да има смисъл да се откажете и да намерите различен канал за разпространение. Ще ви покажа как.

Нашият идеален работен процес DevOps

Идеалният ни работен процес на DevOps е кодът ни да се тества автоматично при всеки комит и автоматично да се изгражда и публикува за разпространение при всеки етикет за издание. Не трябва да има ръчни стъпки между нас, които натискаме промените, и потребителите, които имат достъп до тях.

В този случай, когато казваме „публикуван за разпространение“, имаме предвид, че новата версия на приложението може да бъде инсталирана с помощта на Homebrew. За да постигнем това, ние ще хостваме нашия собствен Homebrew tap в GitHub и ще хостваме нашето изградено приложение в GitHub като актив за освобождаване.

Шестте ръчни стъпки

В контекста на внедряването на софтуер, работата, която има тенденция да бъде „ръчна, повтаряща се, автоматизирана, тактическа, лишена от трайна стойност и която се мащабира линейно“, е известна като „труд“. Има шест ръчни стъпки, които трябва да изпълним, за да пуснем нашето приложение за разпространение след извършване на промени, маркиране и изпращане в GitHub — и всички те са трудоемки:

  • Създайте приложението за пускане.
  • Подпишете приложението с помощта на Apple Developer ID сертификат.
  • Пакетирайте подписаното приложение в дисково изображение.
  • Нотариално заверете изображението на диска.
  • Качете нотариално завереното дисково изображение в GitHub като актив за освобождаване.
  • Актуализирайте крана Homebrew с новата версия на приложението и стойности SHA256.

Кажете сбогом на труда

Какво правим с труда? Ние го автоматизираме! Крайният резултат ще бъде, че всеки път, когато изпратим таг към GitHub, приложението ще бъде автоматично изградено, подписано, пакетирано, нотариално заверено, качено като актив за издание на GitHub и нашият Homebrew кран ще бъде автоматично актуализиран, за да сочи към новата версия. Целият процес ще отнеме само няколко минути.

Автоматизацията е смесица от непрекъсната интеграция (CI) и непрекъснато внедряване (CD), реализирана с помощта на работен процес GitHub Actions.

Непрекъсната интеграция

Първата част от автоматизацията, която създаваме, е непрекъсната интеграция: изграждане и тестване на нашето приложение след всеки ангажимент. За да постигнем това, стартираме xcodebuild test в средата GitHub Actions на macOS хост.

Забележка: Дори ако приложението ви не включва тестове, пак можете да изградите приложението си при всеки ангажимент и да извлечете стойност от непрекъснатата интеграция. В такъв случай използвайте действието build вместо действието test с xcodebuild в последната стъпка от работния процес.

Чрез поставянето на следния работен поток GitHub Actions в нашето работно пространство на проекта на адрес .github/workflows/master_test.yml ние позволяваме непрекъсната интеграция:

Този работен поток се изпълнява всеки път, когато изпращаме нови ангажименти към клона master. Включва едно задание, което се изпълнява на macOS хост. Работата включва три стъпки.

Първо, рефът на ангажимента, който е задействал работния поток, ще бъде рефът, изваден.

След това се определя схемата по подразбиране на проекта и се задава като променлива на средата. (Този подход може да изглежда пресилен на този етап — все пак бихме могли просто да кодираме твърдо името на схемата по подразбиране на нашия проект — но въвеждам този модел сега, защото ще го използваме отново в следващия работен процес и той позволява този работен процес да се използва повторно без модификации с други проекти.)

И накрая, проектът е изграден и тестван с помощта на xcodebuild test, а изходът се предава през xcpretty, удобен инструмент с отворен код, който се предлага предварително инсталиран в GitHub Actions runners. Заменяме настройката за компилация CODE_SIGN_IDENTITY със стойността „“ (тази стойност се нарича „Подписване за локално изпълнение“ в Xcode), за да активираме ad-hoc подписване на код. Това ни позволява да изградим и тестваме приложението на една машина, без да е необходимо да конфигурираме тайни за подписване на код за този работен процес.

За да работи този непрекъснат процес на интегриране, трябва да са верни няколко неща. Горният файл с работен поток трябва да бъде ангажиран към клона master на работното пространство на проекта и изпратен към GitHub. Самият проект трябва да бъде конфигуриран със споделена схема, която съдържа етапи на изграждане и тестване, и тази схема трябва да е първа в списъка със схеми. И накрая, тестовете трябва да могат да се изпълняват, когато се извикват чрез xcodebuild test — проверете това сами, като стартирате тестовете локално от командния ред, преди да разгърнете работния поток за първи път.

Непрекъснато внедряване

Втората част от автоматизацията, която създаваме, е непрекъснатото внедряване: публикуване на нашето приложение след всеки етикет за издание. Ние използваме конвенцията, че всеки етикет, който започва с буквата „v“, е етикет за освобождаване – напр. v1.2.0.

Изпълняваме xcodebuild install в средата GitHub Actions на macOS хост, за да изградим и подпишем проекта, след което пакетираме нашето приложение в дисково изображение, заверяваме го нотариално, качваме го като актив за освобождаване и накрая актуализираме нашия Homebrew кран с най-новия информация за версията.

Чрез поставянето на следния работен поток GitHub Actions в нашето работно пространство на проекта на .github/workflows/master_deploy.yml ние позволяваме непрекъснато внедряване:

Има много повече неща с този работен процес от предишния, но двата започват по подобен начин. Този работен процес се изпълнява всеки път, когато изпращаме нови етикети към хранилището, които започват с буквата „v“. Включва едно задание, което се изпълнява на macOS хост. Работата включва единадесет стъпки.

Както преди, първата стъпка проверява проекта. Този път маркерът, който е задействал работния процес, е маркерът, който е отметнат.

След това се определя схемата по подразбиране на проекта и се задава като променлива на средата, както преди.

След това допълнителните настройки за изграждане от проекта се четат с помощта на xcodebuild -showBuildSettings и се задават като променливи на средата за използване от следващите стъпки. Това избягва необходимостта от твърдо кодиране на детайлите на проекта в работния процес и улеснява повторното използване на работния поток с други проекти.

Следващата стъпка е да настроите macOS ключодържател на хоста за изпълнение на действия, който съдържа идентификационните данни за подписване на нашето приложение, тъй като Xcode очаква да намери самоличността за подписване в ключодържателя по подразбиране. Стъпката използва тайните SIGNING_CERTIFICATE_P12_DATA и SIGNING_CERTIFICATE_PASSWORD и инструмента security за импортиране на сертификата за подписване. (Вижте по-долу за информация как да настроите тайните.) Това е най-сложната и най-малко документирана стъпка от този процес. Отговорите на този въпрос на StackOverflow осигуряват известен контекст.

Сега тестваме приложението. Никога не искаме да пускаме версия на приложението, която не преминава тестовете, така че се уверяваме, че тестовете са преминали, преди да продължим. Ако вашият проект не включва тестове, пропуснете тази стъпка.

След като приложението бъде тествано, следващата стъпка го възстановява за инсталиране и след това инсталира приложението в корен на разпространение (DSTROOT).

След успешно завършване на стъпката на инсталиране, приложението се пакетира в дисково изображение с помощта на hdiutil.

Следващата стъпка в работния процес нотариално заверява образа на диска с помощта на инструмента notarize-cli. Това е инструмент, който написах, за да опростя нотариалното заверяване на приложения за Mac в контекста на непрекъснато внедряване. Той обхваща два инструмента, предоставени с Xcode — altool и stapler. Той използва тези инструменти, за да качи изображението на диска в Apple, да изчака успеха и след това да закрепи нотариалната заверка към изображението на диска. Тази стъпка използва тайните NOTARIZE_USERNAME и NOTARIZE_PASSWORD за удостоверяване на Apple. (Вижте по-долу за информация как да настроите тайните.)

Приложението е готово за внедряване. Следващата стъпка е да доставите нотариално завереното дисково изображение на GitHub като актив за освобождаване. Ние използваме действието softprops/action-gh-release от GitHub marketplace, за да извърши това действие вместо нас. Тази стъпка използва тайната GITHUB_TOKEN, която автоматично се настройва за нас от GitHub Actions. Този токен е с обхват, за да ни позволи да правим модификации само в хранилището, което хоства текущия работен поток.

На този етап приложението е доставено. Някой, който се рови в GitHub, може да намери пуснатото приложение и да го изтегли, но кой има време за това? За да завършим внедряването, все още трябва да актуализираме Homebrew кран. Популярните приложения могат да бъдат хоствани в хранилището на homebrew-cask, но ако приложението ви все още няма аудитория, то няма да бъде прието за включване в официалния кран — ще трябва да хоствате свое собствено. За щастие хостването на вашия собствен кран е лесно и Homebrew поддържа безпроблемно инсталиране от кранове на трети страни. (Вижте по-долу за информация как да настроите кран и бъчва за използване с този работен процес.)

Следващата стъпка в работния процес проверява крана. Тази стъпка използва тайните CASK_REPO и CASK_REPO_TOKEN. (Вижте по-долу за информация относно това как да настроите тайните.) Токенът на cask repo се записва в изведеното работно пространство и се използва за връщане на промените обратно в хранилището за кранове в следващата стъпка.

Последната стъпка е да актуализирате кутията с номера на новата версия на приложението и хеш стойността на актива за освобождаване. Номерът на версията се определя от етикета за освобождаване, който е задействал работния процес, а хешът се изчислява от нотариално завереното дисково изображение. Авторът на ангажимента е зададен на потребителя, който е задействал работния поток, а промените се ангажиментират и връщат обратно към хранилището за кранове.

За да работи този работен процес, няколко неща трябва да са верни. Горният файл с работен поток трябва да бъде ангажиран към клона master на работното пространство на проекта и изпратен към GitHub. Самият проект трябва да бъде конфигуриран със споделена схема по подразбиране, която съдържа етапи на изграждане и тестване. Проектът трябва да бъде конфигуриран с името на самоличността за подписване на код, която да се използва при изграждане за пускане, и това име трябва да съответства на идентификационните данни за подписване, които се съхраняват в тайната на GitHub. И накрая, трябва вече да съществува Cask в хранилище за кранове на Homebrew за последната стъпка за актуализиране.

Конфигурация на проекта

Има привидно безкраен брой начини за конфигуриране на Xcode проект. Надявам се тези работни процеси да работят с вашите. Проверих, че тези работни потоци работят с новосъздаден проект за приложение за macOS, конфигуриран с UI тестове, с изключение на това, че тестът testLaunchPerformance по подразбиране се проваля, когато се изпълнява от командния ред — изтрийте този тест и трябва да сте готови.

За да конфигурирате проект за подписване на ИД на програмист, първо добавете своя Apple ID акаунт в предпочитанията „Акаунти“ на Xcode. След като добавите акаунта, изберете своя екип от таблицата с екипи и щракнете върху бутона „Управление на сертификати…“. Потърсете сертификат „Приложение за ID на програмист“. Ако не виждате такова, създайте го, като щракнете върху бутона „+“ и изберете „Приложение за ID на програмист“ от менюто.

Забележка: Ако създадете сертификата с Xcode, той също трябва автоматично да се импортира във вашия локален ключодържател. Ако по някаква причина трябва ръчно да изтеглите своя сертификат за ИД на програмист и частен ключ и да ги импортирате във вашия ключодържател, можете да го направите от тази страница. Щракнете двукратно върху изтегления „.cer“ файл, за да го импортирате с помощта на приложението „Keychain Access“.

След това направете следните промени в настройките „Подписване и възможности“ за целта на приложението в проекта Xcode:

  • Деактивирайте „Автоматично управление на подписването“.
  • Задайте „Екип“ на екипа, свързан с вашия Apple ID.
  • Задайте „Provisioning Profile“ на „None“].
  • Задайте „Signing Certificate“ на „Developer ID Application“.

Вече трябва да е възможно да използвате Xcode, за да подпишете приложението си с вашия сертификат за ID на програмист. Проверете дали всичко е конфигурирано правилно, като изберете „Архив“ от менюто „Продукт“, за да създадете приложението за пускане. След това изберете “Организатор” от менюто “Прозорец” и намерете новосъздадения архив в таблицата с архиви. Щракнете върху „Разпространяване на приложение“, след което изберете „ИД на програмист“ и щракнете върху „Напред“. Изберете „Експортиране“ и щракнете върху „Напред“. Оставете „Сертификат за разпространение“ и настройките на профила на техните стойности по подразбиране и щракнете върху „Напред“. Въведете идентификационните си данни, ако бъдете подканени, и накрая щракнете върху „Експортиране“, за да запазите подписаното приложение.

Това беше много досадно щракане и чакане — нищо чудно, че решихме да автоматизираме това!

Тайна конфигурация

Създадохме работен поток GitHub Actions за извършване на непрекъсната интеграция и непрекъсната доставка и нашият проект е конфигуриран за подписване на код на ID на програмист. Следващото нещо, което трябва да направите, е да зададете тайните, от които нашият работен процес се нуждае, за да работи успешно.

За да добавите тайна към вашето GitHub хранилище, отворете раздела „Настройки“ на хранилището. След това изберете „Тайни“ от страничната лента. Щракнете върху „Нова тайна“, след което въведете името на тайната и стойността. Накрая щракнете върху „Добавяне на тайна“.

SIGNING_CERTIFICATE_PASSWORD

Тази тайна трябва да бъде зададена на парола, която ще се използва за криптиране на файла p12, който създавате, докато генерирате тайната SIGNING_CERTIFICATE_P12_DATA по-долу.

Ето моя любим един ред за генериране на произволна парола с помощта на командния ред:

< /dev/urandom LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 32; echo

SIGNING_CERTIFICATE_P12_DATA

Тази тайна трябва да бъде зададена на base64-кодирано съдържание на p12 файл, съдържащ сертификата за подписване и частния ключ.

За да зададем тази тайна, първо трябва да експортираме сертификата за подписване и частния ключ от ключодържателя. За да направите това, отворете приложението „Достъп до ключодържател“, уверете се, че ключодържателят ви „влизане“ по подразбиране е избран и след това изберете категорията „Моите сертификати“. Изберете своя сертификат за ID на програмист от списъка и изберете „Експортиране на елементи…“ от менюто „Файл“. Оставете файловия формат зададен на „Обмен на лична информация (.p12)“ и щракнете върху „Запазване“. Въведете стойността на тайната SIGNING_CERTIFICATE_PASSWORD, когато бъдете подканени за парола за шифроване. След това въведете идентификационните данни за вашия локален акаунт, когато бъдете подканени от Keychain Access.

След това отворете терминален прозорец и намерете файла p12 на диска. Изпълнете следната команда, за да кодирате base64 файла и да го копирате в клипборда:

cat Certificates.p12 | base64 | pbcopy

Поставете копирания текст като стойност на тайната.

NOTARIZE_USERNAME: Тази тайна трябва да бъде зададена на Apple ID на вашия акаунт на програмист.

NOTARIZE_PASSWORD: Тази тайна трябва да бъде зададена като парола за конкретно приложение за вашия Apple ID акаунт. Следвайте тези инструкции, за да създадете парола за конкретно приложение.

CASK_REPO: Тази тайна трябва да бъде зададена на пълното име на хранилището на GitHub, което хоства каска за това приложение, напр. bacongravy/homebrew-tap.

CASK_REPO_TOKEN: Тази тайна трябва да бъде зададена на токен за личен достъп (PAT) на GitHub, който има обхват да има достъп за запис до хранилището, посочено от CASK_REPO. За да генерирате PAT, отворете вашите потребителски „Настройки“ в GitHub, изберете „Настройки за разработчици“ от страничната лента, след това изберете „Лични токени за достъп“ от страничната лента, след което щракнете върху „Генериране на нов токен“. Дайте име на токена, изберете обхвата „repo“ и щракнете върху „Генериране на токен“. Копирайте показания токен и го добавете като тайна към вашето хранилище.

Докоснете Конфигурация

Кранът Homebrew е просто хранилище на GitHub, което следва определени конвенции. Прочетете документацията за повече информация.

Предлагам да наименувате вашия кран repositoryhomebrew-tap. Това ще ви позволи да инсталирате приложението си с помощта на команда като следната:

 brew install <username>/tap/<app-name>

Вашето пълно квалифицирано име на хранилище ще бъде <username>/homebrew-tap и това е стойността, на която трябва да зададете тайната CASK_REPO.

Да предположим, че хранилището на вашия проект е наречено „chuck-wagon“, вашият файл на Xcode проект е наречен „chuck-wagon.xcodeproj“, схемата по подразбиране е наречена „Chuck Wagon“ и схемата произвежда продукт, наречен „Chuck Wagon.app“ . В този случай работният процес ще публикува изображение на диска с име „Chuck_Wagon.dmg“, а файлът на бъчвата се очаква да бъде с име „chuck-wagon.rb“.

Ще трябва да създадете cask файла във вашето кран хранилище. Прочетете документацията за повече информация.

Ето как може да изглежда бъчвата за "Chuck Wagon":

За да направите това да работи за вашето приложение, ще трябва да замените „‹username›“, „chuck-wagon“, „Chuck Wagon“, „Chuck Wagon.app“ и „Chuck_Wagon.dmg“ със стойностите, подходящи за вашето потребителско име в GitHub и за вашето приложение.

След като създадете файл във вашето CASK_REPO хранилище на пътя /Casks/$PROJECT_NAME, работният поток master_deploy ще може да актуализира версията и стойностите sha256.

Заключение

Не беше лесно, но се справихме. Честито!

Настроих демо хранилище, за да можете да видите пример на живо. Можете да видите дневник на изпълнение на работния поток master_deploy тук. Бурето за демонстрационното приложение, създадено от този работен процес, може да бъде намерено тук.

Надявам се, че тази статия ви е вдъхновила да автоматизирате труда от живота си. Приятно кодиране!