Русский
Русский
English
Статистика
Реклама

Github actions

Прокачиваем Android проект с GitHub Actions. Часть 2

04.12.2020 10:17:32 | Автор: admin

Запуск UI-тестов на GitHub Actions

Продолжаем разбираться с автоматизацией Android проекта на GitHub Actions, в этой части:

  • Заведем новый проект под UI-тесты в Firebase Test Lab

  • Настроим интеграцию GitHub Actions и Test Lab

  • Посмотрим, как можно запускать UI-тесты в workflow на CI/CD.

Если пропустили первую часть рассказа, где разбирались с Unit-тестами в Android проекте, можно начать с нее.

Чтобы запустить unit-тесты, нам достаточно иметь настроенное Java-окружение. Все тесты проходят очень быстро внутри JVM, всё просто, и почти исключена ситуация появления flacky-тестов. Таких тестов должно быть 70-80% от общего количества тестов в проекте, в первую очередь стоит покрывать ими бизнес-логику.

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

С UI-тестами всё непросто с самого начала. Во-первых, их нужно написать. Звучит банально, но уже на этом этапе у большинства заканчивается энтузиазм. Потом, когда критическая масса UI-тестов написана, нужно придумать, как это богатство запускать и поддерживать в рабочем состоянии в условиях постоянных А/Б-тестов и частых изменений интерфейса. Нужно решать, где они будут запускаться - реальные устройства или эмуляторы и кто будет владеть этими устройствами - своя ферма телефонов или пользоваться услугами сторонних сервисов. В общем, тема не из лёгких.

Мы пойдем по самому простому и удобному пути - Firebase Test Lab

Firebase Test Lab - это сервис от Google, предоставляющий возможность запускать тесты на реальных устройствах или эмуляторах. На момент написания поста бесплатный тариф предлагает 10 тестов в день на эмуляторах и 5 на реальных устройствах. В платном тарифе цена сейчас 1$ за телефоно-час эмулятора, можно и заплатить за такое удобство.

Весь процесс запуска тестов в Test Lab можно описать следующими шагами:

  1. Делаем checkoutна нужный коммит и устанавливаем Java-окружение

  2. Проводим unit-тестирование. Если на этом шаге ошибка, то нет смысла тратить время на UI-тестирование.

  3. Собираем специальными Gradle-тасками артефакты для UI-тестирования

  4. Выкачиваем APK-артефакты, которые собираемся отправить на тестирование.

  5. Авторизуемся в Firebase Test Lab с помощью персонального токена.

  6. Используя специальную command line утилиту gcloud, скармливаемв Test Lab собранные ранее APK.

  7. Ждём, когда тесты пройдут, и смотрим результаты в workflow GitHub Actions.

Но для начала заведём на Firebase проект под приложение и сгенерируем себе токен для доступа к нему из GitHub.

Заходим наhttps://console.firebase.google.comи авторизуемся под своим Google-аккаунтом.

Далее следуем по понятному визарду, нажимаем Создать проект.

Указываем название

Дальше отключаем Google-аналитику или оставляем всё как есть, на Test Lab это никак не повлияет. Если вам нужна аналитика, оставьте и на следующем шаге примите условия пользовательского соглашения.

Когда проект создастся, приступаем к генерации токена для доступа GitHub Actions к Test Lab.

Идём в Настройки проекта (Project settings), затем на вкладку Сервисные аккаунты (Service accounts). Там выбираем Управление правами доступа для сервисных аккаунтов (Manage service account permissions).

Теперь необходимо добавить сервисный аккаунт с теми правами, которые мы планируем использовать на CI/CD в GitHub Actions. Для запуска UI-тестов достаточно прав типа Редактор. Подробнее тут.

Заполняем предлагаемые поля

Выбираем тип Редактор. Если неправильно настроить права доступа для сервисного аккаунта, то на шаге авторизации в Firebase мы получим ошибку 403.

ERROR: (gcloud.firebase.test.android.run) Unable to access the test environment catalog: ResponseError 403: Not authorized for project ***

На третьем шаге можно просто нажать кнопку Готово

Мы только что добавили сервисный аккаунт для CI/CD и теперь готовы получить токен. Выбираем Создать ключ (Create key).

Выбираем из двух предложенных вариантов JSON, после этого он автоматически скачается. Если интересно, что там внутри, - можно открыть в любом текстовом редакторе. Там записана разная служебная информация по вашему аккаунту и проекту плюс private_key.

Фокус в том, что в таком виде JSON у нас не получится использовать. Придётся закодировать его через Base64.

Пути два:

1) В консоли вводим

base64 github-actions-sample-key.json > base64-key.txt

Где github-actions-sample-key.json - это название скачанного на предыдущем шаге JSON, а base64-key файл, в который будет записан результат кодирования.

2) Делаем всё наhttps://www.base64encode.org/

Возвращается в GitHub проект и записываем результат в Secrets в проект на GitHub.

После добавления ключа в секреты обязательно удалите или перенесите в надёжное место ключ с Firebase и base64-key.

Теперь необходимо добавить в секреты Project ID с экрана общих настроек проекта в Firebase. Не перепутайте с Project number.

Отлично, всё готово к интеграции GitHub Actions и Test Lab. Создаём новый workflow в директории giithub/workflows.

Если прямо сейчас запустить workflow, то на прогонах UI-тестов в Test Lab мы получим ошибку.

ERROR: (gcloud.firebase.test.android.run) User [github-actions-ci-cd@***.iam.gserviceaccount.com] does not have permission to access project [***:initializeSettings] (or it may not exist): Cloud Tool Results API has not been used in project 254361894337 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/toolresults.googleapis.com/overview?project=254361894337 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

Вообще-то можно пройти по ссылке из ошибки и включить APItoolresults.googleapis.com, но сейчас посмотрим, как можно управлять вообще любыми API в своём проекте. Нажимаем Enable APIs and services.

Тут можно управлять API в проекте и смотреть статистику использования. Находим Cloud Tool Result API и включаем.

Ну теперь-то уж точно всё, пора запускать workflow.

name: UI_tests_on_release on:  pull_request:    branches:      - 'main' jobs:  assemble_ui_test_artifacts:    if: startsWith(github.head_ref, 'release/') == true    name: Build artifacts    runs-on: ubuntu-20.04    steps:      - uses: actions/checkout@v2      - uses: actions/setup-java@v1        with: {java-version: 1.8}       - name: Build APK for UI test after Unit tests        run: |          ./gradlew test          ./gradlew assembleDebug          ./gradlew assembleDebugAndroidTest       - name: Upload app-debug APK        uses: actions/upload-artifact@v2        with:          name: app-debug          path: app/build/outputs/apk/debug/app-debug.apk       - name: Upload app-debug-androidTest APK        uses: actions/upload-artifact@v2        with:          name: app-debug-androidTest          path: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk   run_ui_tests_on_firebase:    runs-on: ubuntu-20.04    needs: assemble_ui_test_artifacts    steps:      - uses: actions/checkout@v2      - name: Download app-debug APK        uses: actions/download-artifact@v1        with:          name: app-debug       - name: Download app-debug-androidTest APK        uses: actions/download-artifact@v1        with:          name: app-debug-androidTest       - name: Firebase auth with gcloud        uses: google-github-actions/setup-gcloud@master        with:          version: '290.0.1'          service_account_key: ${{ secrets.FIREBASE_KEY }}          project_id: ${{ secrets.FIREBASE_PROJECT_ID }}       - name: Run Instrumentation Tests in Firebase Test Lab        run: |          gcloud firebase test android models list          gcloud firebase test android run --type instrumentation --use-orchestrator --app app-debug/app-debug.apk --test app-debug-androidTest/app-debug-androidTest.apk --device model=Pixel2,version=28,locale=en,orientation=portrait

Разбираемся, что тут вообще происходит

Шаг 1

name: UI_tests_on_release on:  pull_request:    branches:      - 'main' jobs:  assemble_ui_test_artifacts:    if: startsWith(github.head_ref, 'release/') == true    name: Build artifacts    runs-on: ubuntu-20.04    steps:      - uses: actions/checkout@v2      - uses: actions/setup-java@v1        with: {java-version: 1.8}

Тут точно такие же установки для запуска workflow, что и раньше. Pull request в ветку main из ветки, название которой начинается на release/.

Далее делаем checkout и устанавливаем окружение Java 8.

Шаг 2

- name: Build APK for UI test after Unit tests  run: |    ./gradlew test    ./gradlew assembleDebug    ./gradlew assembleDebugAndroidTest - name: Upload app-debug APK  uses: actions/upload-artifact@v2  with:    name: app-debug    path: app/build/outputs/apk/debug/app-debug.apk - name: Upload app-debug-androidTest APK  uses: actions/upload-artifact@v2  with:    name: app-debug-androidTest    path: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk

На этом шаге мы прогоняем unit-тесты и после собираем два APK - app-debug.apk и app-debug-androidTest.apk. Почему два? Да просто один APK - это собственно приложение для тестирования, а второй APK содержит instrumentation-тесты, они оба нам понадобятся.

Дальше достаём полученные артефакты по пути и имени в upload-artifact@v2.

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

Шаг 3

run_ui_tests_on_firebase:  runs-on: ubuntu-20.04  needs: assemble_ui_test_artifacts  steps:    - uses: actions/checkout@v2    - name: Download app-debug APK      uses: actions/download-artifact@v1      with:        name: app-debug     - name: Download app-debug-androidTest APK      uses: actions/download-artifact@v1      with:        name: app-debug-androidTest

Вторая Job в тестовом workflow запускается не параллельно с первой (assemble_ui_test_artifacts), а ждёт, пока та успешно завершится.

Это указано в строчке.

needs: assemble_ui_test_artifacts

Дальше воспользуемся готовым action download-artifact@v1 и достанем по имени те два APK которые собирали в прошлой Job.

Шаг 4

Добрались до самого интересного - пора передавать артефакты в Test Lab для тестирования.

- name: Firebase auth with gcloud  uses: google-github-actions/setup-gcloud@master  with:    version: '290.0.1'    service_account_key: ${{ secrets.FIREBASE_KEY }}    project_id: ${{ secrets.FIREBASE_PROJECT_ID }} - name: Run Instrumentation Tests in Firebase Test Lab  run: |    gcloud firebase test android models list    gcloud firebase test android run --type instrumentation --use-orchestrator --app app-debug/app-debug.apk --test app-debug-androidTest/app-debug-androidTest.apk --device model=Pixel2,version=28,locale=en,orientation=portrait

Сначала выполняем action setup-gcloud, передаем в аргументах ID проекта и тот самый Base64 ключ, который хранится в секретах, всё по инструкции.

Дальше просто выполняем команды в консоли.

Перваяgcloud firebase test android models listвыведет таблицу из доступных устройств с названиями и версиями SDK. Для тестирования это не требуется, просто удобно посмотреть и выбрать подходящее устройство.

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

Запускаем workflow и смотрим, что получилось.

Отлично, всё работает, самый базовый сценарий запуска UI-тестов реализован! Подробные результаты тестирования можно посмотреть на вкладке Test Lab проекта. Или можно убрать из секретов Project ID (иначе в ссылке будут звездочки вместо ID) и переходить на результаты сразу из логов.

Что ещё можно улучшить в сценарии

Можно включить orchestrator, добавив ключ --use-orchestrator.

Оркестратор UI-тестов будет запускать каждый тест в изолированном инстансе, что позволит избежать влияния тестов друг на друга.

--num-flaky-test-attempts - для задания количества попыток перезапуска для Flaky тестов.

--network-profile - для задания профиля сети при тестировании. Можно потестировать на медленном соединении, к примеру.

Полный список ключей с описанием тут:

https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run

Еще можно включить шардинг для запускаемых тестов

https://firebase.google.com/docs/test-lab/android/instrumentation-test#sharding

https://github.com/Flank/flank

Идея для самостоятельного изучения если интересно прямо сейчас что-то посмотреть:

1) Попробовать запустить UI-тесты не в Test Lab а в эмуляторе, поднимаемом на MacOS. Можно посмотретьhttps://github.com/ReactiveCircus/android-emulator-runner

2) Настроить матрицу тестирования для UI-тестов. Запускать тесты не на одной версии Android SDK а на каждой из заданных в условиях.


На этом с запуском UI-тестов из GitHub Actions всё :)

Подробнее..

Пишем простейший GitHub Action на TypeScript

09.06.2021 16:12:52 | Автор: admin

Недавно я решил немного привести в порядок несколько своих .NET pet-проектов на GitHub, настроить для них нормальный CI/CD через GitHub Actions и вынести всё в отдельный репозиторий, чтобы все скрипты лежали в одном месте. Для этого пришлось немного поизучать документацию, примеры и существующие GitHub Actions, выложенные в Marketplace.

В данной статье я решил собрать самую основную информацию, которая нужна для того, чтобы написать простейший GitHub Action с использованием TypeScript.

Статья в первую очередь рассчитана на начинающих, тех, кто никогда не использовал GitHub Actions, но хотел бы быстро начать. Тем не менее, даже если у вас уже есть подобный опыт, но вы, например, не использовали ncc, то, возможно, и для вас в ней будет что-то полезное.

Краткое введение в GitHub Actions

В основе GitHub Actions лежит понятие Workflow - некий рабочий процесс, который запускается по определённым триггерам. В одном репозитории может быть как один рабочий процесс, так и несколько отдельных, срабатывающий при разных условиях.

Чтобы добавить рабочий процесс, нужно создать в репозитории один или несколько yml-файлов в папке .github/workflows. Простейший файл может выглядеть следующим образом:

name: Helloon: [push]jobs:  Hello:    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v2      - name: Hello        run: echo "Hello, GitHub Actions!"

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

Сам рабочий процесс мы можем найти на вкладке Actions в интерфейсе GitHub и посмотреть детальную информацию по нему:

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

Каждый джоб состоит из набора шагов, которые последовательно запускаются внутри одной виртуальной машины. Каждый такой шаг в свою очередь может либо выполнять произвольную команду через run, либо может запускать какое-то действие через uses.

Сами действия представляют из себя отдельный кусок рабочего процесса, который можно повторно использовать в нескольких проектах. Они могут быть трёх видов: докер контейнер, действие JavaScript, или составные действия (которые по сути просто переиспользуемый набор других действий). Вариант с действием JavaScript мы как раз и рассмотрим в этой статье, но писать его будем на TypeScript для большего удобства.

(Более подробную информацию по возможностям рабочих процессов можно найти в документации)

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

Действия можно разместить в репозитории в нескольких местах в зависимости от потребностей:

  • В подпапке .github/actions. Такой способ обычно используется когда вы хотите использовать эти действия из того же репозитория. В этом случае ссылаться на них необходимо по пути без указания ветки или тега:

    steps:  - uses: ./.github/actions/{path}
    
  • В произвольном месте репозитория. Например, можно разместить несколько действий в корне репозитория, каждое в своей подпапке. Такой способ хорошо подходит, если вы хотите сделать что-то вроде личной библиотеки с набором действий, которые собираетесь использовать из разных проектов. В этом случае ссылаться на такие действия нужно по названию репозитория, пути и ветке или тегу:

    steps:  - uses: {owner}/{repo}/{path}@{ref}
    
  • В корне репозитория. Такой способ позволяет разместить в одном репозитории только одно действие, и обычно используется если вы хотите позже опубликовать его в Marketplace. В этом случае ссылаться на такие действия нужно по названию репозитория и ветке или тегу:

    steps:  - uses: {owner}/{repo}@{ref}
    

Создаём действие на TypeScript

В качестве примера создадим очень простое действие, которое просто выводит сообщение Hello, GitHub Actions!. Для разработки действия нам потребуется установленная версия Node.js (я использовал v14.15.5).

Создадим в репозитории папку .github/actions. В ней создадим подпапку hello, в которой будем далее создавать все файлы, относящиеся к нашему действию. Нам потребуется создать всего четыре файла.

Создаём файл action.yml:

name: Hellodescription: Greet someoneruns:  using: node12  main: dist/index.jsinputs:  Name:    description: Who to greet    required: true

Этот файл содержит метаданные нашего действия. По его наличию GitHub понимает, что в папке расположен не случайный набор файлов, а именно какое-то действие, которое можно загрузить и выполнить.

Также мы указываем, что это именно JavaScript действие, а не докер контейнер и указываем точку входа: файл dist/index.js. Этого файла у нас пока нет, но он будет автоматически создан чуть позже.

Кроме этого мы можем указать в этом файле набор входных и выходных параметров. В данном случае мы передаём в качестве параметра имя для приветствия.

(Более подробную информацию по возможностям файла метаданных можно найти в документации)

Создаём файл package.json:

{  "private": true,  "scripts": {    "build": "npx ncc build ./src/main.ts"  },  "dependencies": {    "@actions/core": "^1.2.7",    "@actions/exec": "^1.0.4",    "@actions/github": "^4.0.0",    "@actions/glob": "^0.1.2",    "@types/node": "^14.14.41",    "@vercel/ncc": "^0.28.3",    "typescript": "^4.2.4"  }}

Это стандартный файл для Node.js. Чтобы не указывать бесполезные атрибуты типа автора, лицензии и т.п. можно просто указать, что пакет private. (При желании можно конечно всё это указать, кто я такой, чтобы вам это запрещать =)

В скриптах мы указываем один единственный скрипт сборки, который запускает утилиту ncc. Эта утилита на вход получает файл src/main.ts и создаёт файл dist/index.js, который является точкой входа для нашего действия. Я вернусь к этой утилите чуть позже.

В качестве зависимостей мы указываем typescript и @types/node для работы TypeScript. Зависимость @vercel/ncc нужна для работы ncc.

Зависимости @actions/* опциональны и являются частью GitHub Actions Toolkit - набора пакетов для разработки действий (я перечислил самые на мой взгляд полезные, но далеко не все):

  • Зависимость @actions/core понадобится вам вероятнее всего. Там содержится базовый функционал по выводу логов, чтению параметров действия и т.п. (документация)

  • Зависимость @actions/exec нужна для запуска других процессов, например dotnet. (документация)

  • Зависимость @actions/github нужна для взаимодействия с GitHub API. (документация)

  • Зависимость @actions/glob нужна для поиска файлов по маске. (документация)

Стоит также отметить, что формально, поскольку мы будем компилировать наше действие в один единственный файл dist/index.js через ncc, все зависимости у нас будут зависимостями времени разработки, т.е. их правильнее помещать не в dependencies, а в devDependencies. Но по большому счёту никакой разницы нет, т.к. эти зависимости вообще не будут использоваться во время выполнения.

Создаём файл tsconfig.json:

{  "compilerOptions": {    "target": "ES6",    "module": "CommonJS",    "moduleResolution": "Node",    "strict": true  }}

Тут всё достаточно просто. Это минимальный файл, с которым всё хорошо работает, включая нормальную подсветку синтаксиса и IntelliSense в Visual Studio Code.

Создаём файл src/main.ts:

import * as core from '@actions/core'async function main() {  try {    const name = core.getInput('Name');    core.info(`Hello, ${name}`);  } catch (error) {    core.setFailed(error.message)  }}main();

В качестве точки входа мы используем синхронную или асинхронную функцию main, которую вызываем в этом же файле. (Название функции также может быть любым).

Тело функции необходимо обернуть в блок try-catch, чтобы в случае любых ошибок сборка в GitHub прерывалась. Без этого она всегда будет считаться успешной.

В данном конкретном случае мы также используем пакет @actions/core для чтения параметров и вывода сообщения в лог.

Собираем действие при помощи ncc

После того, как все файлы созданы нам первым делом необходимо восстановить все пакеты из npm. Для этого нам нужно перейти в папку действия (не в корень репозитория) и выполнить команду:

npm install

А дальше возникает нюанс. Если прочитать официальную документацию, чтобы GitHub мог корректно загрузить и выполнить действие, все скрипты, включая зависимости должны находиться в том же репозитории. Это означает, что нам по сути предлагается закоммитить папку node_modules в репозиторий, что, на мой взгляд, не очень красивое решение.

В качестве альтернативы можно воспользоваться пакетом @vercel/ncc, который позволяет собрать js или ts-файлы в один единственный js-файл, который уже можно закоммитить в репозиторий.

Поскольку мы уже указали скрипт для сборки в файле package.json, нам осталось только запустить команду:

npm run build

В результате мы получим файл dist/index.js, который нужно будет закоммитить в репозиторий вместе с остальными файлами. Папка node_modules при этом может быть спокойно отправлена в .gitignore.

Используем действие

Чтобы протестировать действие, создадим в папке .github/workflows файл рабочего процесса. Для разнообразия сделаем так, чтобы он запускался не по пушу, а вручную из интерфейса:

name: Helloon:  workflow_dispatch:    inputs:      Name:        description: Who to greet        required: true        default: 'GitHub Actions'jobs:  Hello:    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v2      - name: Hello        uses: ./.github/actions/hello        with:          Name: ${{ github.event.inputs.Name }}

Здесь настройки workflow_dispatch описывают форму в интерфейсе GitHub в которую пользователь сможет ввести данные. Там у нас будет одно единственное поле для ввода имени для приветствия.

Данные, введённые в форму через событие передаются в действие, которое мы запускаем в рабочем процессе через параметр github.event.inputs.Name.

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

После того, как мы запушим наш рабочий процесс, мы можем перейти в интерфейс GitHub, на странице Actions выбрать наш рабочий процесс и запустить его выполнение, указав параметры:

После того, как мы запустим рабочий процесс, можем зайти в него и посмотреть, что получилось:

Настраиваем GitHooks для автоматической сборки

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

Каждый раз, когда мы будем изменять код действия нам нужно не забыть вызвать команду:

npm run build

По моему опыту такой ручной запуск постоянно будет приводить к ситуации "Я вроде поменял код и запушил его на сервер, но у меня ничего не поменялось. А, блин, я же забыл пересобрать файлы...".

Решить эту проблему можно несколькими способами. Можно не париться и действительно каждый раз запускать сборку руками. В некоторых проектах люди подходят к этому основательно и делают специальный рабочий процесс через тот же GitHub Actions, который на каждый коммит пытается пересобрать действие и коммитит изменения.

Я для себя себя решил проблему проще и просто добавил гит хук, который автоматически пересобирает все действия в репозитории на каждый коммит. Для этого необходимо создать в репозитории папку .githooks, а в ней файл pre-commit следующего вида:

#!/bin/shfor action in $(find ".github/actions" -name "action.yml"); do    action_path=$(dirname $action)    action_name=$(basename $action_path)    echo "Building \"$action_name\" action..."    pushd "$action_path" >/dev/null    npm run build    git add "dist/index.js"    popd >/dev/null    echodone

Здесь мы находим все файлы action.yml в папке .github/actions и для всех найденных файлов запускаем сборку из их папки. Теперь нам не нужно будет думать о явной пересборке наших действий, она будет делаться автоматически.

Чтобы хуки из папки .githooks запускались, нам необходимо поменять конфигурацию для текущего репозитория:

git config core.hooksPath .githooks

Или можно сделать это глобально (я сделал именно так):

git config --global core.hooksPath .githooks

Заключение

В данной статье я постарался собрать основную информацию, необходимую для того, чтобы быстро сделать первый шаг к GitHub Actions и начать писать свои собственные скрипты автоматизации на TypeScript.

Репозиторий с примером можно найти тут: https://github.com/Chakrygin/hello-github-action

Подробнее..

Из песочницы Использование GitHub Actions с C и CMake

24.06.2020 14:16:49 | Автор: admin

Привет, Хабр! Предлагаю вашему вниманию перевод статьи "Using GitHub Actions with C++ and CMake" о сборке проекта на C++ с использованием GitHub Actions и CMake автора Кристиана Адама.


Использование GitHub Actions с C++ и CMake


В этом посте я хочу показать файл конфигурации GitHub Actions для проекта C++, использующего CMake.


GitHub Actions это предоставляемая GitHub инфраструктура CI/CD. Сейчас GitHub Actions предлагает следующие виртуальные машины (runners):


Виртуальное окружение Имя рабочего процесса YAML
Windows Server 2019 windows-latest
Ubuntu 18.04 ubuntu-latest or ubuntu-18.04
Ubuntu 16.04 ubuntu-16.04
macOS Catalina 10.15 macos-latest

Каждая виртуальная машина имеет одинаковые доступные аппаратные ресурсы:


  • 2х ядерное CPU
  • 7 Гб оперативной памяти
  • 14 Гб на диске SSD

Каждое задание рабочего процесса может выполняться до 6 часов.


К сожалению, когда я включил GitHub Actions в проекте C++, мне предложили такой рабочий процесс:


./configuremakemake checkmake distcheck

Это немного не то, что можно использовать с CMake.


Hello World


Я хочу собрать традиционное тестовое приложение C++:


#include <iostream>int main(){  std::cout << "Hello world\n";}

Со следующим проектом CMake:


cmake_minimum_required(VERSION 3.16)project(main)add_executable(main main.cpp)install(TARGETS main)enable_testing()add_test(NAME main COMMAND main)

TL;DR смотрите проект на GitHub.


Матрица сборки


Я начал со следующей матрицы сборки:


name: CMake Build Matrixon: [push]jobs:  build:    name: ${{ matrix.config.name }}    runs-on: ${{ matrix.config.os }}    strategy:      fail-fast: false      matrix:        config:        - {            name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",            os: windows-latest,            build_type: "Release", cc: "cl", cxx: "cl",            environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"          }        - {            name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",            os: windows-latest,            build_type: "Release", cc: "gcc", cxx: "g++"          }        - {            name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",            os: ubuntu-latest,            build_type: "Release", cc: "gcc", cxx: "g++"          }        - {            name: "macOS Latest Clang", artifact: "macOS.tar.xz",            os: macos-latest,            build_type: "Release", cc: "clang", cxx: "clang++"          }

Свежие CMake и Ninja


На странице установленного ПО виртуальных машин мы видим, что CMake есть везде, но в разных версиях:


Виртуальное окружение Версия CMake
Windows Server 2019 3.16.0
Ubuntu 18.04 3.12.4
macOS Catalina 10.15 3.15.5

Это значит, что нужно будет ограничить минимальную версию CMake до 3.12 или обновить CMake.


CMake 3.16 поддерживает прекомпиляцию заголовков и Unity Builds, которые помогают сократить время сборки.


Поскольку у CMake и Ninja есть репозитории на GitHub, я решил скачать нужные релизы с GitHub.


Для написания скрипта я использовал CMake, потому что виртуальные машины по умолчанию используют свойственный им язык скриптов (bash для Linux и powershell для Windows). CMake умеет выполнять процессы, загружать файлы, извлекать архивы и делать еще много полезных вещей.


- name: Download Ninja and CMake  id: cmake_and_ninja  shell: cmake -P {0}  run: |    set(ninja_version "1.9.0")    set(cmake_version "3.16.2")    message(STATUS "Using host CMake version: ${CMAKE_VERSION}")    if ("${{ runner.os }}" STREQUAL "Windows")      set(ninja_suffix "win.zip")      set(cmake_suffix "win64-x64.zip")      set(cmake_dir "cmake-${cmake_version}-win64-x64/bin")    elseif ("${{ runner.os }}" STREQUAL "Linux")      set(ninja_suffix "linux.zip")      set(cmake_suffix "Linux-x86_64.tar.gz")      set(cmake_dir "cmake-${cmake_version}-Linux-x86_64/bin")    elseif ("${{ runner.os }}" STREQUAL "macOS")      set(ninja_suffix "mac.zip")      set(cmake_suffix "Darwin-x86_64.tar.gz")      set(cmake_dir "cmake-${cmake_version}-Darwin-x86_64/CMake.app/Contents/bin")    endif()    set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")    file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)    set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")    file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)    # Save the path for other steps    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)    message("::set-output name=cmake_dir::${cmake_dir}")    if (NOT "${{ runner.os }}" STREQUAL "Windows")      execute_process(        COMMAND chmod +x ninja        COMMAND chmod +x ${cmake_dir}/cmake      )    endif()

Шаг настройки


Теперь, когда у меня есть CMake и Ninja, все, что мне нужно сделать, это настроить проект таким образом:


- name: Configure  shell: cmake -P {0}  run: |    set(ENV{CC} ${{ matrix.config.cc }})    set(ENV{CXX} ${{ matrix.config.cxx }})    if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")      execute_process(        COMMAND "${{ matrix.config.environment_script }}" && set        OUTPUT_FILE environment_script_output.txt      )      file(STRINGS environment_script_output.txt output_lines)      foreach(line IN LISTS output_lines)        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")          set(ENV{${CMAKE_MATCH_1}} "${CMAKE_MATCH_2}")        endif()      endforeach()    endif()    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/ninja" ninja_program)    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake        -S .        -B build        -D CMAKE_BUILD_TYPE=${{ matrix.config.build_type }}        -G Ninja        -D CMAKE_MAKE_PROGRAM=${ninja_program}      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Bad exit status")    endif()

Я установил переменные окружения CC и CXX, а для MSVC мне пришлось выполнить скрипт vcvars64.bat, получить все переменные окружения и установить их для выполняющегося скрипта CMake.


Шаг сборки


Шаг сборки включает в себя запуск CMake с параметром --build:


- name: Build  shell: cmake -P {0}  run: |    set(ENV{NINJA_STATUS} "[%f/%t %o/sec] ")    if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")      file(STRINGS environment_script_output.txt output_lines)      foreach(line IN LISTS output_lines)        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")          set(ENV{${CMAKE_MATCH_1}} "${CMAKE_MATCH_2}")        endif()      endforeach()    endif()    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake --build build      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Bad exit status")    endif()

Что бы увидеть скорость компиляции на разном виртуальном окружении, я установил переменную NINJA_STATUS.


Для переменных MSVC я использовал скрипт environment_script_output.txt, полученный на шаге настройки.


Шаг запуска тестов


На этом шаге вызывается ctest с передачей числа ядер процессора через аргумент -j:


- name: Run tests  shell: cmake -P {0}  run: |    include(ProcessorCount)    ProcessorCount(N)    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/ctest -j ${N}      WORKING_DIRECTORY build      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Running tests failed!")    endif()

Шаги установки, упаковки и загрузки


Эти шаги включают запуск CMake с --install, последующий вызов CMake для создания архива tar.xz и загрузку архива как артефакта сборки.


- name: Install Strip  run: ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake --install build --prefix instdir --strip- name: Pack  working-directory: instdir  run: ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake -E tar cJfv ../${{ matrix.config.artifact }} .- name: Upload  uses: actions/upload-artifact@v1  with:    path: ./${{ matrix.config.artifact }}    name: ${{ matrix.config.artifact }}

Я не стал использовать CMake в качестве языка сценариев для простых вызовов CMake с параметрами, оболочки по умолчанию прекрасно с этим справляются.


Обработка релизов


Когда вы помечаете релиз в git, вы также хотите, чтобы артефакты сборки прикрепились к релизу:


git tag -a v1.0.0 -m "Release v1.0.0"git push origin v1.0.0

Ниже приведён код для этого, который сработает, если git refpath содержит tags/v:


release:  if: contains(github.ref, 'tags/v')  runs-on: ubuntu-latest  needs: build  steps:  - name: Create Release    id: create_release    uses: actions/create-release@v1.0.0    env:      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}    with:      tag_name: ${{ github.ref }}      release_name: Release ${{ github.ref }}      draft: false      prerelease: false  - name: Store Release url    run: |      echo "${{ steps.create_release.outputs.upload_url }}" > ./upload_url  - uses: actions/upload-artifact@v1    with:      path: ./upload_url      name: upload_urlpublish:  if: contains(github.ref, 'tags/v')  name: ${{ matrix.config.name }}  runs-on: ${{ matrix.config.os }}  strategy:    fail-fast: false    matrix:      config:      - {          name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",          os: ubuntu-latest        }      - {          name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",          os: ubuntu-latest        }      - {          name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",          os: ubuntu-latest        }      - {          name: "macOS Latest Clang", artifact: "macOS.tar.xz",          os: ubuntu-latest        }  needs: release  steps:  - name: Download artifact    uses: actions/download-artifact@v1    with:      name: ${{ matrix.config.artifact }}      path: ./  - name: Download URL    uses: actions/download-artifact@v1    with:      name: upload_url      path: ./  - id: set_upload_url    run: |      upload_url=`cat ./upload_url`      echo ::set-output name=upload_url::$upload_url  - name: Upload to Release    id: upload_to_release    uses: actions/upload-release-asset@v1.0.1    env:      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}    with:      upload_url: ${{ steps.set_upload_url.outputs.upload_url }}      asset_path: ./${{ matrix.config.artifact }}      asset_name: ${{ matrix.config.artifact }}      asset_content_type: application/x-gtar

Это выглядит сложным, но это необходимо, так как actions/create-release можно вызвать однократно, иначе это действие закончится ошибкой. Это обсуждается в issue #14 и issue #27.


Несмотря на то, что вы можете использовать рабочий процесс до 6 часов, токен secrets.GITHUB_TOKEN действителен один час. Вы можете создать личный токен или загрузить артефакты в релиз вручную. Подробности в обсуждении сообщества GitHub.


Заключение


Включить GitHub Actions в вашем проекте на CMake становится проще, если создать файл .github/workflows/build_cmake.yml с содержимым из build_cmake.yml.


Вы можете посмотреть GitHub Actions в моем проекте Hello World GitHub.


Оригинальный текст опубликован под лицензией CC BY 4.0.

Подробнее..

Взламываем Ball Sort Puzzle

08.01.2021 12:15:36 | Автор: admin
Определение кружочков при помощи OpenCVОпределение кружочков при помощи OpenCV

Ball Sort Puzzle это популярная мобильная игра на IOS/Android. Суть её заключается в перестановке шариков до тех пор, пока в колбах не будут шарики одного цвета. При этом шарик можно перетаскивать либо в пустую колбу, либо на такой же шарик.

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

Во-первых, игра бесконечна почти бесконечна. По крайней мере уже сейчас на YouTube есть прохождения всех уровней в плоть до 5350, а в телеграмме гуляют скриншоты 10к+ уровней. Вторая особенность, и вот это уже некрасиво, не у всех уровней есть решение.

Ну это ни в какие ворота против нас играет коварный ИИ. Нужно действовать соответственно!

Под катом мы:

  • Придумаем алгоритм, решающий эту головоломку (Python)

  • Научимся парсить скриншот игры, чтобы скармливать алгоритму задачки (OpenCV)

  • Напишем телеграм бота, который будет принимать скриншоты и возвращать решения

  • Выстроим CI/CD через GitHub Actions и задеплоим бота на Яндекс.Функции

Погнали!


Алгоритмическое решение задачи

Решать такую задачу было весьма занимательно. Поэтому предлагаю заинтересованному читателю попробовать решить её самостоятельно.

Я же в первую очередь решил побить проблему на сущности. Это сделает алгоритм чуть более элегантным, а так же поможет в будущем парсить скриншоты игры:

class Color
class Color:    def __init__(self, symbol, verbose_name, emoji):        self.symbol = symbol        self.verbose_name = verbose_name        self.emoji = emoji    def __repr__(self) -> str:        return f'Color({self})'    def __str__(self) -> str:        return self.emoji
Beta-редактор хабра ломается на рендеринге emoji :poop:Beta-редактор хабра ломается на рендеринге emoji :poop:
class Ball
class Ball:    def __init__(self, color: Color):        self.color = color    def __eq__(self, other: 'Ball'):        return self.color is other.color    def __repr__(self):        return f'Ball({self.color.verbose_name})'    def __str__(self) -> str:        return str(self.color)
class Flask
class Flask:    def __init__(self, column: List[Color], num: int, max_size: int):        self.num = num        self.balls = [Ball(color) for color in column]        self.max_size = max_size    @property    def is_full(self):        return len(self.balls) == self.max_size    @property    def is_empty(self) -> bool:        return not self.balls    def pop(self) -> Ball:        return self.balls.pop(-1)    def push(self, ball: Ball):        self.balls.append(ball)    def __iter__(self):        return iter(self.balls)    def __getitem__(self, item: int) -> Ball:        return self.balls[item]    def __len__(self) -> int:        return len(self.balls)    def __str__(self) -> str:        return str(self.balls)
class Move
class Move:    def __init__(self, i, j, i_color: Color):        self.i = i        self.j = j        self.emoji = i_color.emoji    def __eq__(self, other: 'Move') -> bool:        return (self.i, self.j) == (other.i, other.j)    def __repr__(self) -> str:        return f'Ball({self})'    def __str__(self) -> str:        return f'{self.i} -> {self.j}'

Для решения будем использовать метод поиска с возвратом (Backtracking).

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

В случае с нашей игрой это метод применяется так: мы рекурсивно обходим все возможные перестановки шариков (move) до тех пор, пока

  • Либо нас не выкинет наш критерий остановки решённый пазл

  • Либо в нашем хранилище состояний (states) не будет всех возможных перестановок в таком случае решения нет

    def solve(self) -> bool:        if self.is_solved:            return True        for move in self.get_possible_moves():            new_state = self.commit_move(move)            if new_state in self.states:  # Cycle!                self.rollback_move(move)                continue            self.states.add(new_state)            if self.solve():                return True            self.rollback_move(move)        return False

Алгоритм достаточно прямолинейный и далеко не всегда выдаёт оптимальное решение. Тем не менее он справляется с решением большинства задачек из игры за 1 сек.

Проверим алгоритм на чём-нибудь попроще:

def test_3x3():    data_in = [        [color.RED, color.GREEN, color.RED],        [color.GREEN, color.RED, color.GREEN],        [],    ]    puzzle = BallSortPuzzle(data_in)    result = puzzle.solve()    assert result is True    play_moves(data_in, puzzle.moves)
Алгоритм в действииАлгоритм в действии

Полная версия программы доступна на github.

Распознавание скриншотов игры

Мы будем работать с .jpg картинками двух видов

Скриншоты уровней игры Скриншоты уровней игры

Каждый чётный раунд игры состоит из 11 колб и 36 шариков, а нечётный 14 колб и 48 шариков. Чётные и нечётные раунды отличаются расположением колб, но на счастье всё остальное у них одинаковое по 4 шарика в колбе, 2 колбы пустые, цвета используются одни и те же.

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

class ImageParser:    def __init__(self, file_bytes: np.ndarray, debug=False):        self.image_orig = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)        self.image_cropped = self.get_cropped_image(self.image_orig)    @staticmethod    def get_cropped_image(image):        height, width, _ = image.shape        quarter = int(height / 4)        cropped_img = image[quarter : height - quarter]        return cropped_img
Рабочая областьРабочая область

Теперь будем искать кружочки. В библиотеке OpenCV ровно для этих целей существует метод HoughCircles. Чтобы его использовать нужно перевести изображение в чёрно-белый вид, а также "эмпирически подобрать" параметры поиска. Чтобы найденные кружочки потом расфасовать по колбам, нормализуем и отсортируем их.

    @staticmethod    def normalize_circles(circles):        last_y = 0        for circle in circles:            if math.isclose(circle[1], last_y, abs_tol=3):                circle[1] = last_y            else:                last_y = circle[1]        return circles    def get_normalized_circles(self) -> List[Any]:        image_cropped_gray = cv2.cvtColor(self.image_cropped, cv2.COLOR_BGR2GRAY)        circles = cv2.HoughCircles(image_cropped_gray, cv2.HOUGH_GRADIENT, 2, 20, maxRadius=27)        if circles is None:            raise ImageParserError("No circles :shrug:")        circles = np.round(circles[0, :]).astype("int16")        ind = np.lexsort((circles[:, 0], circles[:, 1]))        circles = circles[ind]        circles = self.normalize_circles(circles)        ind = np.lexsort((circles[:, 0], circles[:, 1]))        circles = circles[ind]        return circles
Отсортированные шарики слева-направо, сверху-внизОтсортированные шарики слева-направо, сверху-вниз

Дальше будем определять цвет шарика.

Из-за того, что Telegram жмёт картинки мы не можем просто взять цвет центрального пикселя он может быть артефактом компрессии. Поэтому найдём доминирующий цвет, тот который в кружочке встречается чаще всего.

    @staticmethod    def get_dominant_color(circle) -> Color:        colors, count = np.unique(circle.reshape(-1, circle.shape[-1]), axis=0, return_counts=True)        dominant = colors[count.argmax()]        return dominant
Найденные кружочки Найденные кружочки

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

d = \sqrt{(r2-r1)^2 + (b2-b1)^2 + (g2-g1)^2}

Посчитаем такое расстояние до каждого из изначально заданных цветов и найдём минимальное

RBG_TO_COLOR = {    (147, 42, 115): VIOLET,    (8, 74, 125): BROWN,    (229, 163, 85): L_BLUE,    (68, 140, 234): ORANGE,    (196, 46, 59): BLUE,    (51, 100, 18): GREEN,    (35, 43, 197): RED,    (87, 216, 241): YELLOW,    (125, 214, 97): L_GREEN,    (123, 94, 234): PINK,    (16, 150, 120): LIME,    (102, 100, 99): GRAY,}COLORS = np.array(list(RBG_TO_COLOR.keys()))def get_closest_color(color: np.ndarray) -> Color:    distances = np.sqrt(np.sum((COLORS - color) ** 2, axis=1))    index_of_smallest = np.where(distances == np.amin(distances))    smallest_distance = COLORS[index_of_smallest].flat    return RBG_TO_COLOR[tuple(smallest_distance)]  # type: ignore

Далее нам остаётся только распределить шарики по колбам. Итоговый class ImageParser доступен на github.

Преобразуем программу в Telegram Bot

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

Так как наш бот хоститься на Яндекс.Функции триггером к его запуску будет запрос на заданный нами webhook.

Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serializedUpdate.

Если в сообщении есть массив photo, то можно увеличить вероятность распознавания шариков выбрав фотографию с максимальным весом:

if photos := message.get('photo'):    # here photos is an array with same photo of different sizes    # get one with the highest resolution    hd_photo = max(photos, key=lambda x: x['file_size'])

Чтобы скачать картинку, придётся сделать 2 запроса к Telegram API

# Получение данных о файле, нас интересует ключ ответа file_pathGET https://api.telegram.org/bot{BOT_TOKEN}/getFile?file_id={file_id}# Получение самого файлаGET https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}

В остальном же всё просто получаем картинку, скармливаем её парсеру и затем алгоритму-решателю.

main.py
def handler(event: Optional[dict], context: Optional[dict]):    body = json.loads(event['body'])  # type: ignore    print(body)    message = body['message']    chat_id = message['chat']['id']    if photos := message.get('photo'):        # here photos is an array with same photo of different sizes        hd_photo = max(photos, key=lambda x: x['file_size'])  # get one with the highest resolution        try:            file = telegram_client.download_file(hd_photo['file_id'])        except TelegramClientError:            text = "Cant download the image from TG :("        else:            file_bytes = np.asarray(bytearray(file.read()), dtype=np.uint8)            try:                image_parser = ImageParser(file_bytes)                colors = image_parser.to_colors()            except ImageParserError as exp:                text = f"Cant parse image: {exp}"            else:                puzzle = BallSortPuzzle(colors)  # type: ignore                solved = puzzle.solve()                if solved:                    text = get_telegram_repr(puzzle)                else:                    text = "This lvl don't have a solution"    else:        return {            'statusCode': 200,            'headers': {'Content-Type': 'application/json'},            'body': '',            'isBase64Encoded': False,        }    msg = {        'method': 'sendMessage',        'chat_id': chat_id,        'text': text,        'parse_mode': 'Markdown',        'reply_to_message_id': message['message_id'],    }    return {        'statusCode': 200,        'headers': {'Content-Type': 'application/json'},        'body': json.dumps(msg, ensure_ascii=False),        'isBase64Encoded': False,    }

Отмечу ещё один нюанс: телеграм очень строго следует политике экранирования спецсимволов. Для Markdown это:

To escape characters '_', '*', '`', '[' outside of an entity, prepend the characters '\' before them.

Любой такой неэкранированный символ и вы не увидите ответа в телеграм-чате. И останется только гадать является ли это ошибка интеграции или вот такой коварный баг. Будьте осторожны.

Деплой бота в Яндекс.Функцию

Про создание Я.Функции также есть отличная статья от @mzaharov. Там подробно описан процесс заведения функции, а также установки вебхука для телеграмм бота.

Я расскажу как сделал Continuous Delivery при помощи GitHub Actions. Каждая сборка мастера увенчивается деплоем новой версии функции. Такой подход заставляет придерживаться модели разработки GithubFlow с его главным манифестом

Anything in themasterbranch is always deployable.

Каждая сборка мастера состоит из 3ёх этапов

  • lint (black, flake8, isort, mypy) проверка кода на соответствие всем стандартам Python 2020

  • test тестируем программу с помощью pytest, поддерживая качество и покрытие кода

  • deploy непосредственно заливаем новую версию приложения в облако

Деплоить будем с помощью Yandex-Serveless-Action уже готового Action для использования в своих пайплайнах

  deploy:    name: deploy    needs: pytest    runs-on: ubuntu-latest    if: github.ref == 'refs/heads/master'    steps:      - uses: actions/checkout@master      - uses: goodsmileduck/yandex-serverless-action@v1        with:          token: ${{ secrets.YC_TOKEN }}          function_id: ${{ secrets.YC_FUNCTION_ID }}          runtime: 'python38'          memory: '256'          execution_timeout: "120"          entrypoint: 'main.handler'          environment: "\            TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}"          source: 'app'

Переменные окружения программы и сборки спрячем в GitHub Secrets на уровне репозитория.

Результат

Пример работы @ballsortpuzzlebotПример работы @ballsortpuzzlebot

Бота можно найти в telegram по позывному @ballsortpuzzlebot.

Все исходники на Github.

Присоединяйтесь к маленькому community любителей этой игры в telegram. Бот был добавлен в группу и внимательно следит за всеми отправленными картинками.

Бонус! Уровни, у которых нет решения
Lvl 4091Lvl 4091Lvl 6071Lvl 6071

Мой алгоритм умывает руки говорит что перебрал все возможные комбинации и решения нет. Возможно это баг алгоритма.. или QA-отдел мобильной игры просто забил на эти уровни, так как не предполагал, что кто-то так далеко зайдёт)

Заключение

Для меня это был интересный опыт скрещивания технологий (Telegram API + Python + OpenCV + Lambda). Надеюсь он окажется полезен кому-нибудь ещё.

Найденные баги, предложения по оптимизации алгоритма, или даже PR в репозиторий крайне приветствуются

С новым годом!

Подробнее..

Рефакторинг пет проекта докеризация, метрики, тесты

17.02.2021 20:19:33 | Автор: admin

Всем привет, я php разработчик. Я хочу поделиться историей, как я рефакторил один из своих телеграм ботов, который из поделки на коленке стал сервисом с более чем 1000 пользователей в очень узкой и специфической аудитории.

Предыстория

Пару лет назад я решил тряхнуть стариной и поиграть в LineAge II на одном из популярных пиратских серверов. В этой игре есть один игровой процесс, в котором требуется "поговорить" с ящиками после смерти 4 боссов. Ящик стоит после смерти 2 минуты. Сами боссы после смерти появляются спустя 24 +/- 6ч, то есть шанс появится есть как через 18ч, так и через 30ч. У меня на тот момент была фуллтайм работа, да и в целом не было времени ждать эти ящики. Но нескольким моим персонажам требовалось пройти этот квест, поэтому я решил "автоматизировать" этот процесс. На сайте сервера есть RSS фид в формет XML, где публикуются события с серверов, включая события смерти босса.

Задумка была следующей:

  • получить данные с RSS

  • сравнить данные с локальной копией в базе данных

  • если есть разница данных - сообщить об этом в телеграм канал

  • отдельно сообщать если босса не убили за первые 9ч сообщением "осталось 3ч", и "осталось 1,5ч". Допустим вечером пришло сообщение, что осталось 3ч, значит смерть босса будет до того, как я пойду спать.

Код на php был написан быстро и в итоге у меня было 3 php файла. Один был с god object классом, а другие два запускали программу в двух режимах - парсер новых, или проверка есть ли боссы на максимальном "респе". Запускал я их крон командами. Это работало и решало мою проблему.

Другие игроки замечали, что я появляюсь в игре сразу после смерти боссов, и через 10 дней у меня на канале было около 50 подписчиков. Так же попросили сделать такое же для второго сервера этого пиратского сервиса. Задачу я тоже решил копипастой. В итоге у меня уже 4 файла с почти одинаковым кодом, и файл с god object. Потом меня попросили сделать то же самое для третьего сервера этого пиратского сервиса. И это отлично работало полтора года.

В итоге у меня спустя полтора года:

  • у меня 6 файлов, дублируют себя почти полностью (по 2 файла на сервер)

  • один god object на несколько сотен строк

  • MySQL и Redis на сервере, где разместил код

  • cron задачи, которые запускают файлы

  • ~1400 подписчиков на канале в телеграм

Я откладывал месяцами рефакторинг этого кода, как говориться "работает - не трогай". Но хотелось этот проект привести в порядок, чтобы проще было вносить изменения, легче запускать и переносить на другой сервер, мониторить работоспособность и тд. При этом сделать это за выходные, в свое личное время.

Ожидаемый результат после рефакторинга

  1. Отрефакторить код так, чтобы легче было вносить изменения. Важный момент - отрефакторить без изменения бизнес логики, по сути раскидать god object по файлам, сам код не править, иначе это затянет сроки. Следовать PSR-12.

  2. Докеризировать воркера для удобства переноса на другой сервер и прозрачность запуска и остановки

  3. Запускать воркера через supervisor

  4. Внедрить процесс тестирования кода, настроить Codeception

  5. Докеризировать MySQL и Redis

  6. Настроить Github Actions для запуска тестов и проверки на code style

  7. Поднять Prometheus, Grafana для метрик и мониторинга работоспособности

  8. Сделать докер контейнер, который будет отдавать метрики на страницу /metrics для Prometheus

  9. Сделать докер образ для бота телеграм, который будет отдавать срез по всем статусам 4 боссов в данный момент командами боту в личку

Важное замечание. Все эти шаги выполнялись не совсем в том порядке, как я их описываю в этом туториале. Сделал все требуемое за выходные плюс пара вечеров после работы. Так же в целях не было сделать проект "идеальным", не совершать "революции", а дать возможность проекту плавно эволюционировать. Большая часть пунктов из плана давала возможность развивать проект.

Шаг 1. Рефакторинг приложения

Одним из требований было не потратить на это недели, поэтому основные классы я решил сделать наследниками Singleton

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Support;use AsteriosBot\Core\Exception\DeserializeException;use AsteriosBot\Core\Exception\SerializeException;class Singleton{    protected static $instances = [];    /**     * Singleton constructor.     */    protected function __construct()    {        // do nothing    }    /**     * Disable clone object.     */    protected function __clone()    {        // do nothing    }    /**     * Disable serialize object.     *     * @throws SerializeException     */    public function __sleep()    {        throw new SerializeException("Cannot serialize singleton");    }    /**     * Disable deserialize object.     *     * @throws DeserializeException     */    public function __wakeup()    {        throw new DeserializeException("Cannot deserialize singleton");    }    /**     * @return static     */    public static function getInstance(): Singleton    {        $subclass = static::class;        if (!isset(self::$instances[$subclass])) {            self::$instances[$subclass] = new static();        }        return self::$instances[$subclass];    }}

Таким образом вызов любого класса, который от него наследуются, можно делать методом getInstance()

Вот так, например, выглядел класс подключения к базе данных

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Connection;use AsteriosBot\Core\App;use AsteriosBot\Core\Support\Config;use AsteriosBot\Core\Support\Singleton;use FaaPz\PDO\Database as DB;class Database extends Singleton{    /**     * @var DB     */    protected DB $connection;    /**     * @var Config     */    protected Config $config;    /**     * Database constructor.     */    protected function __construct()    {        $this->config = App::getInstance()->getConfig();        $dto = $this->config->getDatabaseDTO();        $this->connection = new DB($dto->getDsn(), $dto->getUser(), $dto->getPassword());    }    /**     * @return DB     */    public function getConnection(): DB    {        return $this->connection;    }}

В процессе рефакторинга я не менял саму бизнес логику, оставил все "как было". Цель было именно разнести по файлам для облегчения изменения правок, а так же для возможности потом покрыть тестами.

Шаг 2: Докеризация воркеров

Запуск всех контейнеров я сделал через docker-compose.yml

Конфиг сервиса для воркеров выглядит так:

  worker:    build:      context: .      dockerfile: docker/worker/Dockerfile    container_name: 'asterios-bot-worker'    restart: always    volumes:      - .:/app/    networks:      - tier

А сам docker/worker/Dockerfile выглядит так:

FROM php:7.4.3-alpine3.11# Copy the application codeCOPY . /appRUN apk update && apk add --no-cache \    build-base shadow vim curl supervisor \    php7 \    php7-fpm \    php7-common \    php7-pdo \    php7-pdo_mysql \    php7-mysqli \    php7-mcrypt \    php7-mbstring \    php7-xml \    php7-simplexml \    php7-openssl \    php7-json \    php7-phar \    php7-zip \    php7-gd \    php7-dom \    php7-session \    php7-zlib \    php7-redis \    php7-session# Add and Enable PHP-PDO ExtenstionsRUN docker-php-ext-install pdo pdo_mysqlRUN docker-php-ext-enable pdo_mysql# RedisRUN apk add --no-cache pcre-dev $PHPIZE_DEPS \        && pecl install redis \        && docker-php-ext-enable redis.so# Install PHP ComposerRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer# Remove CacheRUN rm -rf /var/cache/apk/*# setup supervisorADD docker/supervisor/asterios.conf /etc/supervisor/conf.d/asterios.confADD docker/supervisor/supervisord.conf /etc/supervisord.confVOLUME ["/app"]WORKDIR /appRUN composer installCMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

Обратите внимание на последнюю строку в Dockerfile, там я запускаю supervisord, который будет мониторить работу воркеров.

Шаг 3: Настройка supervisor

Важный дисклеймер по supervisor. Он предназначен для работы с процессами, которые работают долго, и в случае его "падения" - перезапустить. Мои же php скрипты работали быстро и сразу завершались. supervisor пробовал их перезапустить, и в конце концов переставал пытаться поднять снова. Поэтому я решил сам код воркера запускать на 1 минуту, чтобы это работало с supervisor.

Код файла worker.php

<?phprequire __DIR__ . '/vendor/autoload.php';use AsteriosBot\Channel\Checker;use AsteriosBot\Channel\Parser;use AsteriosBot\Core\App;use AsteriosBot\Core\Connection\Log;$app = App::getInstance();$checker = new Checker();$parser = new Parser();$servers = $app->getConfig()->getEnableServers();$logger = Log::getInstance()->getLogger();$expectedTime = time() + 60; // +1 min in seconds$oneSecond = time();while (true) {    $now = time();    if ($now >= $oneSecond) {        $oneSecond = $now + 1;        try {            foreach ($servers as $server) {                $parser->execute($server);                $checker->execute($server);            }        } catch (\Throwable $e) {            $logger->error($e->getMessage(), $e->getTrace());        }    }    if ($expectedTime < $now) {        die(0);    }}

У RSS есть защита от спама, поэтому пришлось сделать проверку на секунды и посылать не более 1го запроса в секунду. Таким образом мой воркер каждую секунду выполняет 2 действия, сначала проверяет rss, а затем калькулирует время боссов для сообщений о старте или окончании времени респауна боссов. После 1 минуты работы воркер умирает, и его перезапускает supervisor

Сам конфиг supervisor выглядит так:

[program:worker]command = php /app/worker.phpstderr_logfile=/app/logs/supervisor/worker.lognumprocs = 1user = rootstartsecs = 3startretries = 10exitcodes = 0,2stopsignal = SIGINTreloadsignal = SIGHUPstopwaitsecs = 10autostart = trueautorestart = truestdout_logfile = /dev/stdoutstdout_logfile_maxbytes = 0redirect_stderr = true

После старта контейнеров супервизор стартует воркера автоматически. Важный момент - в файле основного конфига /etc/supervisord.confобязательно нужно указать демонизация процесса, а так же подключение своих конфигов

[supervisord]nodaemon=true[include]files = /etc/supervisor/conf.d/*.conf

Набор полезных команд supervisorctl:

supervisorctl status       # статус воркеровsupervisorctl stop all     # остановить все воркераsupervisorctl start all    # запустить все воркераsupervisorctl start worker # запустить один воркера с конфига, блок [program:worker]

Шаг 4: Настройка Codeception

Я планирую в свободное время по чуть-чуть покрывать unit тестами уже существующий код, а со временем сделать еще и интеграционные. Пока что настроил только юнит тестирование и написал пару тестов на особо важную бизнес логику. Настройка была тривиальной, все завелось с коробки, только добавил в конфиг поддержку базы данны

# Codeception Test Suite Configuration## Suite for unit or integration tests.actor: UnitTestermodules:    enabled:        - Asserts        - \Helper\Unit        - Db:              dsn: 'mysql:host=mysql;port=3306;dbname=test_db;'              user: 'root'              password: 'password'              dump: 'tests/_data/dump.sql'              populate: true              cleanup: true              reconnect: true              waitlock: 10              initial_queries:                - 'CREATE DATABASE IF NOT EXISTS test_db;'                - 'USE test_db;'                - 'SET NAMES utf8;'    step_decorators: ~

Шаг 5: Докеризация MySQL и Redis

На сервере, где работало это приложение, у меня было еще пара других ботов. Все они использовали один сервер MySQL и один Redis для кеша. Я решил вынести все, что связано с окружением в отельный docker-compose.yml, а самих ботов залинковать через внешний docker network

Выглядит это так:

version: '3'services:  mysql:    image: mysql:5.7.22    container_name: 'telegram-bots-mysql'    restart: always    ports:      - "3306:3306"    environment:      MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"      MYSQL_ROOT_HOST: '%'    volumes:      - ./docker/sql/dump.sql:/docker-entrypoint-initdb.d/dump.sql    networks:      - tier  redis:    container_name: 'telegram-bots-redis'    image: redis:3.2    restart: always    ports:      - "127.0.0.1:6379:6379/tcp"    networks:      - tier  pma:    image: phpmyadmin/phpmyadmin    container_name: 'telegram-bots-pma'    environment:      PMA_HOST: mysql      PMA_PORT: 3306      MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"    ports:      - '8006:80'    networks:      - tiernetworks:  tier:    external:      name: telegram-bots-network

DB_PASSWORD я храню в .env файле, а ./docker/sql/dump.sql у меня лежит бекап для инициализации базы данных. Так же я добавил external network так же, как в этом конфиге - в каждом docker-compose.yml каждого бота на сервере. Таким образом они все находятся в одной сети и могут использовать общие базу данных и редис.

Шаг 6: Настройка Github Actions

В шаге 4 этого туториала я добавил тестовый фреймфорк Codeception, который для тестирования требует базу данных. В самом проекте нет базы, в шаге 5 я ее вынес отдельно и залинковал через external docker network. Для запуска тестов в Github Actions я решил полностью собрать все необходимое на лету так же через docker-compose.

name: Actionson:  pull_request:    branches: [master]  push:    branches: [master]jobs:  build:    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v2      - name: Get Composer Cache Directory        id: composer-cache        run: |          echo "::set-output name=dir::$(composer config cache-files-dir)"      - uses: actions/cache@v1        with:          path: ${{ steps.composer-cache.outputs.dir }}          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}          restore-keys: |            ${{ runner.os }}-composer-      - name: Composer validate        run: composer validate      - name: Composer Install        run: composer install --dev --no-interaction --no-ansi --prefer-dist --no-suggest --ignore-platform-reqs      - name: PHPCS check        run: php vendor/bin/phpcs --standard=psr12 app/ -n      - name: Create env file        run: |          cp .env.github.actions .env      - name: Build the docker-compose stack        run: docker-compose -f docker-compose.github.actions.yml -p asterios-tests up -d      - name: Sleep        uses: jakejarvis/wait-action@master        with:          time: '30s'      - name: Run test suite        run: docker-compose -f docker-compose.github.actions.yml -p asterios-tests exec -T php vendor/bin/codecept run unit

Инструкция onуправляет когда билд триггернётся. В моем случае - при создании пулл реквеста или при коммите в мастер.

Инструкция uses: actions/checkout@v2 запускает проверку доступа процесса к репозиторию.

Далее идет проверка кеша композера, и установка пакетов, если в кеше не найдено

Затем в строке run: php vendor/bin/phpcs --standard=psr12 app/ -nя запускаю проверку кода соответствию стандарту PSR-12 в папке ./app

Так как тут у меня специфическое окружение, я подготовил файл .env.github.actionsкоторый копируется в .env Cодержимое .env.github.actions

SERVICE_ROLE=testTG_API=XXXXXTG_ADMIN_ID=123TG_NAME=AsteriosRBbotDB_HOST=mysqlDB_NAME=rootDB_PORT=3306DB_CHARSET=utf8DB_USERNAME=rootDB_PASSWORD=passwordLOG_PATH=./logs/DB_NAME_TEST=test_dbREDIS_HOST=redisREDIS_PORT=6379REDIS_DB=0SILENT_MODE=trueFILLER_MODE=true

Из важного тут только настройки базы данных, которые не должны отличаться от настроек базы в этом окружении.

Затем я собираю проект при помощи docker-compose.github.actions.ymlв котором прописано все необходимое для тестирвания, контейнер с проектом и база данных. Содержимое docker-compose.github.actions.yml:

version: '3'services:  php:    build:      context: .      dockerfile: docker/php/Dockerfile    container_name: 'asterios-tests-php'    volumes:      - .:/app/    networks:      - asterios-tests-network  mysql:    image: mysql:5.7.22    container_name: 'asterios-tests-mysql'    restart: always    ports:      - "3306:3306"    environment:      MYSQL_DATABASE: asterios      MYSQL_ROOT_PASSWORD: password    volumes:      - ./tests/_data/dump.sql:/docker-entrypoint-initdb.d/dump.sql    networks:      - asterios-tests-network##  redis:#    container_name: 'asterios-tests-redis'#    image: redis:3.2#    ports:#      - "127.0.0.1:6379:6379/tcp"#    networks:#      - asterios-tests-networknetworks:  asterios-tests-network:    driver: bridge

Я закомментировал контейнер с Redis, но оставил возможность использовать его в будущем. Сборка с кастомным docker-compose файлом, а затем тесты - запускается так

docker-compose -f docker-compose.github.actions.yml -p asterios-tests up -ddocker-compose -f docker-compose.github.actions.yml -p asterios-tests exec -T php vendor/bin/codecept run unit

Внимательный читатель обратит внимание на пункт между стартом контейнеров и запуском тестов. Это задержка в 30 секунд для того, чтобы база данных успела заполниться тестовыми данными.

Шаг 7: Настройка Prometheus и Grafana

В шаге 5 я вынес MySQL и Redis в отдельный docker-compose.yml. Так как Prometheus и Grafana тоже общие для всех моих телеграм ботов, я их добавил туда же. Сам конфиг этих контейнеров выглядит так:

  prometheus:    image: prom/prometheus:v2.0.0    command:      - '--config.file=/etc/prometheus/prometheus.yml'    restart: always    ports:      - 9090:9090    volumes:      - ./prometheus.yml:/etc/prometheus/prometheus.yml    networks:      - tier  grafana:    container_name: 'telegram-bots-grafana'    image: grafana/grafana:7.1.1    ports:      - 3000:3000    environment:      - GF_RENDERING_SERVER_URL=http://renderer:8081/render      - GF_RENDERING_CALLBACK_URL=http://grafana:3000/      - GF_LOG_FILTERS=rendering:debug    volumes:      - ./grafana.ini:/etc/grafana/grafana.ini      - grafanadata:/var/lib/grafana    networks:      - tier    restart: always  renderer:    image: grafana/grafana-image-renderer:latest    container_name: 'telegram-bots-grafana-renderer'    restart: always    ports:      - 8081    networks:      - tier

Они так же залинкованы одной сетью, которая потом линкуется с external docker network.

Prometheus: я прокидываю свой конфиг prometheus.yml, где я могу указать источники для парсинга метрик

Grafana: я создаю volume, где будут храниться конфиги и установленные плагины. Так же я прокидываю ссылку на сервис рендеринга графиков, который мне понадобиться для отправки alert. С этим плагином alert приходит со скриншотом графика.

Поднимаю проект и устанавливаю плагин, затем перезапускаю Grafana контейнер

docker-compose up -ddocker-compose exec grafana grafana-cli plugins install grafana-image-rendererdocker-compose stop  grafana docker-compose up -d grafana

Шаг 8: Публикация метрик приложения

Для сбора и публикации метрик я использовал endclothing/prometheus_client_php

Так выглядит мой класс для метрик

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Connection;use AsteriosBot\Core\App;use AsteriosBot\Core\Support\Singleton;use Prometheus\CollectorRegistry;use Prometheus\Exception\MetricsRegistrationException;use Prometheus\Storage\Redis;class Metrics extends Singleton{    private const METRIC_HEALTH_CHECK_PREFIX = 'healthcheck_';    /**     * @var CollectorRegistry     */    private $registry;    protected function __construct()    {        $dto = App::getInstance()->getConfig()->getRedisDTO();        Redis::setDefaultOptions(            [                'host' => $dto->getHost(),                'port' => $dto->getPort(),                'database' => $dto->getDatabase(),                'password' => null,                'timeout' => 0.1, // in seconds                'read_timeout' => '10', // in seconds                'persistent_connections' => false            ]        );        $this->registry = CollectorRegistry::getDefault();    }    /**     * @return CollectorRegistry     */    public function getRegistry(): CollectorRegistry    {        return $this->registry;    }    /**     * @param string $metricName     *     * @throws MetricsRegistrationException     */    public function increaseMetric(string $metricName): void    {        $counter = $this->registry->getOrRegisterCounter('asterios_bot', $metricName, 'it increases');        $counter->incBy(1, []);    }    /**     * @param string $serverName     *     * @throws MetricsRegistrationException     */    public function increaseHealthCheck(string $serverName): void    {        $prefix = App::getInstance()->getConfig()->isTestServer() ? 'test_' : '';        $this->increaseMetric($prefix . self::METRIC_HEALTH_CHECK_PREFIX . $serverName);    }}

Для проверки работоспособности парсера мне нужно сохранить метрику в Redis после получения данных с RSS. Если данные получены, значит все нормально, и можно сохранить метрику

        if ($counter) {            $this->metrics->increaseHealthCheck($serverName);        }

Где переменная $counter это количество записей в RSS. Там будет 0, если получить данные не удалось, и значит метрика не будет сохранена. Это потом понадобится для alert по работе сервиса.

Затем нужно метрики опубликовать на странице /metric чтобы Prometheus их спарсил. Добавим хост в конфиг prometheus.yml из шага 7.

# my global configglobal:  scrape_interval:     5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.  # scrape_timeout is set to the global default (10s).scrape_configs:  - job_name: 'bots-env'    static_configs:      - targets:          - prometheus:9090          - pushgateway:9091          - grafana:3000          - metrics:80 # тут будут мои метрики по uri /metrics

Код, который вытащит метрики из Redis и создаст страницу в текстовом формате. Эту страничку будет парсить Prometheus

$metrics = Metrics::getInstance();$renderer = new RenderTextFormat();$result = $renderer->render($metrics->getRegistry()->getMetricFamilySamples());header('Content-type: ' . RenderTextFormat::MIME_TYPE);echo $result;

Теперь настроим сам дашборд и alert. В настройках Grafana сначала укажите свой Prometheus как основной источник данных, а так же я добавил основной канал нотификации Телеграм (там добавляете токен своего бота и свой chat_id с этим ботом)

Настройка GrafanaНастройка Grafana
  1. Метрика increase(asterios_bot_healthcheck_x3[1m]) Показывает на сколько метрика asterios_bot_healthcheck_x3 увеличилась за 1 минуту

  2. Название метрики (будет под графиком)

  3. Название для легенды в пункте 4.

  4. Легенда справа из пункта 3.

  1. Правило, по которому проверяется метрика. В моем случае проверяет что за последние 30 секунд проблем не было

  2. Правило, по которому будет срабатывать alert. В моем случае "Когда сумма из метрики А между сейчас и 10 секунд назад"

  3. Если нет данных вообще - слать alert

  4. Сообщение в alert

Выглядит alert в телеграм так (помните мы настраивали рендеринг картинок для alert?)

Alert в ТелеграмAlert в Телеграм
  1. Обратите внимание, alert заметил падение, но все восстановилось. Grafana приготовилась слать alert, но передумала. Это то самое правило 30 секунд

  2. Тут уже все упало больше чем на 30 секунд и alert был отправлен

  3. Сообщение, которое мы указали в настройках alert

  4. Ссылка на dashboard

  5. Источник метрики

Шаг 9: Телеграм бот

Настройка телеграм бота ничем не отличается от настройки воркера. Телеграм бот по сути у меня это еще один воркер, я запустил его при помощи добавления настроек в supervisor. Тут уже рефакторинг проекта дал свои плоды, запуск бота был быстрым и простым.

Итоги

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

Ссылки на проекты

Подробнее..

Автоматизация ручных действий с GitHub Actions

12.01.2021 18:23:11 | Автор: admin

GitHub Actions инструмент для автоматизации рутинных действий с вашего пакета на GitHub.

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

GitHub предоставляет действительно удобные и рабочие инструменты для этого.

План действий

  • настроим CI в GitHub Actions для небольшого проекта на PHP

  • научимся запускать тесты в матрице с покрытием (зачем это нужно также расскажу)

  • создадим ботов, которые будут назначать ревьюющих / исполнителей, выставлять метки для PR-s (на основе измененных файлов), а по окончании ревью и проверок в Check Suite будут автоматом мержить наши PR, а сами ветки будут удаляться автоматически.

  • подключим бота, который будет создавать релизы, которые автоматически будут пушиться в packagist.

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

Настройка CI

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

Создайте файл примерно с таким содержимым:

.github/workflows/ci.yml
name: CIon:  push:    branches:      - master  pull_request:    types:      - opened      - reopened      - edited      - synchronizeenv:  COVERAGE: '1'  php_extensions: 'pdo, pdo_pgsql, pcntl, pcov, ...'  key: cache-v0.1  DB_USER: 'postgres'  DB_NAME: 'testing'  DB_PASSWORD: 'postgres'  DB_HOST: '127.0.0.1'jobs:  lint:    runs-on: '${{ matrix.operating_system }}'    timeout-minutes: 20    strategy:      matrix:        operating_system: ['ubuntu-latest']        php_versions: ['7.4']      fail-fast: false    env:      PHP_CS_FIXER_FUTURE_MODE: '0'    name: 'Lint PHP'    steps:      - name: 'Checkout'        uses: actions/checkout@v2      - name: 'Setup cache environment'        id: cache-env        uses: shivammathur/cache-extensions@v1        with:          php-version: '${{ matrix.php_versions }}'          extensions: '${{ env.php_extensions }}'          key: '${{ env.key }}'      - name: 'Cache extensions'        uses: actions/cache@v1        with:          path: '${{ steps.cache-env.outputs.dir }}'          key: '${{ steps.cache-env.outputs.key }}'          restore-keys: '${{ steps.cache-env.outputs.key }}'      - name: 'Setup PHP'        uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php_versions }}          extensions: '${{ env.php_extensions }}'          ini-values: memory_limit=-1          tools: pecl, composer          coverage: none      - name: 'Setup problem matchers for PHP (aka PHP error logs)'        run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'      - name: 'Setup problem matchers for PHPUnit'        run: 'echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"'      - name: 'Install PHP dependencies with Composer'        run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader        working-directory: './'      - name: 'Linting PHP source files'        run: 'composer lint'  test:    strategy:      fail-fast: false      matrix:        operating_system: ['ubuntu-latest']        postgres: [11, 12]        php_versions: ['7.3', '7.4', '8.0']        experimental: false        include:          - operating_system: ubuntu-latest            postgres: '13'            php_versions: '8.0'            experimental: true       runs-on: '${{ matrix.operating_system }}'    services:      postgres:        image: 'postgres:${{ matrix.postgres }}'        env:          POSTGRES_USER: ${{ env.DB_USER }}          POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}          POSTGRES_DB: ${{ env.DB_NAME }}        ports:          - 5432:5432        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5    name: 'Test / PHP ${{ matrix.php_versions }} / Postgres ${{ matrix.postgres }}'    needs:      - lint    steps:      - name: 'Checkout'        uses: actions/checkout@v2        with:          fetch-depth: 1      - name: 'Install postgres client'        run: |          sudo apt-get update -y          sudo apt-get install -y libpq-dev postgresql-client      - name: 'Setup cache environment'        id: cache-env        uses: shivammathur/cache-extensions@v1        with:          php-version: ${{ matrix.php_versions }}          extensions: ${{ env.php_extensions }}          key: '${{ env.key }}'      - name: 'Cache extensions'        uses: actions/cache@v1        with:          path: '${{ steps.cache-env.outputs.dir }}'          key: '${{ steps.cache-env.outputs.key }}'          restore-keys: '${{ steps.cache-env.outputs.key }}'      - name: 'Setup PHP'        uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php_versions }}          extensions: ${{ env.php_extensions }}          ini-values: 'pcov.directory=src, date.timezone=UTC, upload_max_filesize=20M, post_max_size=20M, memory_limit=512M, short_open_tag=Off'          coverage: pcov          tools: 'phpunit'      - name: 'Install PHP dependencies with Composer'        run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader        working-directory: './'      - name: 'Run Unit Tests with PHPUnit'        continue-on-error: ${{ matrix.experimental }}        run: |          sed -e "s/\${USERNAME}/${{ env.DB_USER }}/" \              -e "s/\${PASSWORD}/${{ env.DB_PASSWORD }}/" \              -e "s/\${DATABASE}/${{ env.DB_NAME }}/" \              -e "s/\${HOST}/${{ env.DB_HOST }}/" \              phpunit.xml.dist > phpunit.xml          ./vendor/bin/phpunit \            --verbose \            --stderr \            --coverage-clover build/logs/clover.xml        working-directory: './'      - name: 'Upload coverage results to Coveralls'        if: ${{ !matrix.experimental }}        env:          COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}          COVERALLS_PARALLEL: true          COVERALLS_FLAG_NAME: php-${{ matrix.php_versions }}-postgres-${{ matrix.postgres }}        run: |          ./vendor/bin/php-coveralls \            --coverage_clover=build/logs/clover.xml \            -v  coverage:    needs: test    runs-on: ubuntu-latest    name: "Code coverage"    steps:      - name: 'Coveralls Finished'        uses: coverallsapp/github-action@v1.1.2        with:          github-token: ${{ secrets.GITHUB_TOKEN }}          parallel-finished: true

Расскажу лишь в кратце, в этом конфиге 3 основных шага (lint, tests и coverage)

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

Если код не соответствует code style проекта, джобка падает и CI дальше не запускается. В данном примере, я использую линтер с правилами от umbrellio/code-style-php, а сами скрипты запуска описаны так (первый для проверки, второй для авто фиксов для локального использования):

"scripts": {   "lint": "ecs check --config=ecs.yml .",   "lint-fix": "ecs check --config=ecs.yml . --fix"}

test - тестируем наше приложение в матрице следующего ПО (os, postgres и php), а через опцию include добавляем что-то дополнительное (важно сохранить структуру ключей матрицы).

В целом тут тоже ничего нет сложного, разве что два момента:

  • опция experimental (к слову назвать опцию можно как угодно) для матрицы нужна для того, чтобы падающие джобки в экспериментальном окружении не фейлили CI, например, когда вы добавляете поддержку новой версии PHP, и не ставите самоцелью решать упавшие тесты или покрытие прям щас. Такие джобки игнорируются (если падают).

  • строки с sed -e "s/\${USERNAME}/${{ env.DB_USER }}/"... нужны для того, чтобы переменные подключения к БД были записаны из файла phpunit.xml.dist с плейсхолдерами в phpunit.xml, это не панацея, вы можете использовать переменные окружения ENV, но на всякий случай файл доступен тут:

phpunit.xml.dist
<?xml version="1.0" encoding="UTF-8"?><phpunit xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"         bootstrap="vendor/autoload.php"         colors="true"         convertErrorsToExceptions="true"         convertNoticesToExceptions="true"         convertWarningsToExceptions="true"         processIsolation="false"         stopOnFailure="false"         xsi:noNamespaceSchemaLocation="http://personeltest.ru/aways/schema.phpunit.de/9.3/phpunit.xsd">    <php>        <env name="APP_ENV" value="testing"/>        <ini name="error_reporting" value="-1" />        <var name="db_type" value="pdo_pgsql"/>        <var name="db_host" value="${HOST}" />        <var name="db_username" value="${USERNAME}" />        <var name="db_password" value="${PASSWORD}" />        <var name="db_database" value="${DATABASE}" />        <var name="db_port" value="5432"/>    </php>    <filter>        <whitelist processUncoveredFilesFromWhitelist="true">            <directory suffix=".php">./src</directory>            <exclude>                <file>./src/.meta.php</file>            </exclude>        </whitelist>    </filter>    <testsuites>        <testsuite name="Test suite">            <directory suffix="Test.php">./tests</directory>        </testsuite>    </testsuites></phpunit>

coverage - т.к. тестирование и покрытие также происходит в матрице, т.к. часть кода может быть написана под одну версию Postgres, а другая под другую и оформлено в виде условий в вашем коде, то покрыть на 100% за одну итерацию может быть невозможно. К сожалению, composer, в отличие от Bandler-а от Ruby, так делать не умеет.

Но т.к. я перфекционист и мне нужен badge:100% coverage, в моем случае используется матрица покрытия и затем, отправленные отчеты о покрытии, мержатся в один. Например, coveralls.io поддерживает обьединенный кавераж.

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

Авто-назначение меток (labels)

Для подключения бота создайте два файла (конфиг и скрипт):

.github/labeler.config.yml
type:build:  - ".github/**/*"  - ".coveralls.yml"  - ".gitignore"  - "ecs.yml"  - "phpcs.xml"dependencies:  - "composer.json"  - "composer.lock"type:common  - "src/**/*"type:tests:  - 'tests/**/*'  - 'phpunit.xml.dist'  - 'tests.sh'theme:docs:  - "README.md"  - "LICENSE"  - "CONTRIBUTING.md"  - "CODE_OF_CONDUCT.md"

Тут по сути мы описываем маппинг меток к файлам и директориям вашего пакета, в зависимости от того, какие файлы будут изменены в рамках вашего PR, такие метки будут автоматически выставлены к PR.

Метки нужны для того, чтобы в последствии мы могли на их основании генерировать Summary для наших релизов и определять степень важности PR (будет ли это patch, minor или major). Вообще говоря, метки помогают визуально категоризировать пулл-реквесты, что очень удобно, когда их (pull-реквестов) много.

.github/workflows/labeler.yml
name: "Auto labeling for a pull request"on:  - pull_request_targetjobs:  triage:    name: "Checking for labels"    runs-on: ubuntu-latest    steps:      - uses: actions/labeler@main        with:          repo-token: "${{ secrets.GITHUB_TOKEN }}"          sync-labels: true          configuration-path: ".github/labeler.config.yml"

Авто-назначение ревьюеров и исполнителей

Для подключения бота создайте два файла (конфиг и скрипт):

.github/assignee.config.yml
addReviewers: truenumberOfReviewers: 1reviewers: - pvsaitpeaddAssignees: trueassignees: - pvsaintpenumberOfAssignees: 1skipKeywords:  - wip  - draft

Тут мы по сути описываем, кто и в каком кол-ве будет назначаться в качестве ревьюеров, и просто перечисляем логины пользователей с GitHub.

.github/workflows/assignee.yml
name: 'Auto assign assignees or reviewers'on: pull_requestjobs:  add-reviews:    name: "Auto assignment of a assignee"    runs-on: ubuntu-latest    steps:      - uses: kentaro-m/auto-assign-action@v1.1.2        with:          configuration-path: ".github/assignee.config.yml"

Авто-мержирование проверенных PR

Для подключения бота создайте файл скрипта с содержимым:

.github/workflows/auto_merge.yml
name: 'Auto merge of approved pull requests with passed checks'on:  pull_request:    types:      - labeled      - unlabeled      - synchronize      - opened      - edited      - ready_for_review      - reopened      - unlocked  pull_request_review:    types:      - submitted  check_suite:    types:      - completed  status: {}jobs:  automerge:    runs-on: ubuntu-latest    steps:      - name: 'Automerge PR'        uses: "pascalgn/automerge-action@v0.12.0"        env:          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"          MERGE_METHOD: 'squash'          MERGE_LABELS: "approved,!work in progress"          MERGE_REMOVE_LABELS: "approved"          MERGE_COMMIT_MESSAGE: "pull-request-description"          MERGE_RETRIES: "6"          MERGE_RETRY_SLEEP: "10000"          UPDATE_LABELS: ""          UPDATE_METHOD: "rebase"          MERGE_DELETE_BRANCH: false

Из важного тут только то, что мержится будут только те PR, у которых будет выставлена метка approved, а также если все проверки в CheckSuite будут пройдены.

Мержить будем через Squash, чтобы была красивая история коммитов.

Авто-апрув отревьюенных PR

Когда ревьюющий ставит аппрув в PR, будем автоматом проставлять метку approved, создайте файл скрипта с содержимым:

.github/workflows/auto_approve.yml
on: pull_request_reviewname: 'Label approved pull requests'jobs:  labelWhenApproved:    name: 'Label when approved'    runs-on: ubuntu-latest    steps:      - name: 'Label when approved'        uses: pullreminders/label-when-approved-action@master        env:          APPROVALS: "1"          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}          ADD_LABEL: "approved"          REMOVE_LABEL: "awaiting review"

Авто-выпуск релизов с ченджлогом

Для подключения бота создайте два файла (конфиг и скрипт) с содержимым:

.github/release-drafter.yml
template: |  ## Changes  $CHANGESchange-template: '- **$TITLE** (#$NUMBER)'version-template: "$MAJOR.$MINOR.$PATCH"name-template: '$RESOLVED_VERSION'tag-template: '$RESOLVED_VERSION'categories:  - title: 'Features'    labels:      - 'feature'      - 'type:common'  - title: 'Bug Fixes'    labels:      - 'fix'      - 'bugfix'      - 'bug'      - 'hotfix'      - 'dependencies'  - title: 'Maintenance'    labels:      - 'type:build'      - 'refactoring'      - 'theme:docs'      - 'type:tests'change-title-escapes: '\<*_&'version-resolver:  major:    labels:      - major      - refactoring  minor:    labels:      - feature      - minor      - type:common  patch:    labels:      - patch      - type:build      - bug      - bugfix      - hotfix      - fix      - theme:docs      - type:tests  default: patch

В зависимости от меток, бот будет увеличивать либо MAJOR, либо MINOR, либо версию PATCH

.github/workflows/release_drafter.yml
name: Release Drafteron:  push:    branches:      - masterjobs:  update_release_draft:    runs-on: ubuntu-latest    steps:      - uses: release-drafter/release-drafter@v5        with:          publish: true        env:          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Теперь нужно провести некоторые настройки в GitHub Settings вашего проекта

Настройка Check Suite в GitHub

По умолчанию ветки в GitHub никак не ограничены, и пушить в них может каждый, кто имеет доступ на запись, но если вы хотите, чтобы код был красивый, чтобы код был покрыт на 100%, и у вас есть прочие хотелки, необходимо поставить ограничения и настроить Check Suite.

Пример, где настраиваются ограничения веток

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

Настройка approvals

По сути, тут мы настраиваем кол-во людей, которые должны посмотреть PR, будут ли сбрасываться апрувы, после появления новых коммитов, а также необходимо ли участие Code Owners в ревью.

Пример, как настраиваются approvalls

Настройка обязательных проверок для Check Suite

Все наши проверки (в CI это джобки, в основном, но и другие интеграции тоже, например, Coveralls / Scrutinizer, и прочие анализаторы кода), могут быть как обязательными или необязательными.

Если проверка обязательная, то мержирование PR будет заблокировано пока все проверки не будут пройдены.

Пример, как настроить Check Suite для ветки

Автоматически удаляем ветки после мержа

Чтобы у нас была красивая история коммитов, а также чтобы не удалять вручную ветки после мержа, в Settings => Options нужно разрешить только Squash, если вы хотите красивую историю коммитов и включить опцию "Automatically delete head branches"

Пример настройки тут

Настройка веб-хука для packagist.org

Тут все стандартно, на сайте packagist есть инструкция, но для полноты поста выложу тоже.

Пример, как настроить webhook packagist

Секретный ключ можно взять на packagist в настройках вашего профиля (Show Api Token).

Таким образом, если вы поддерживаете достаточное кол-во OpenSource проектов, и в каждом из них есть некоторое количество активных Contributor-ов (с правами записи), вы можете настроить CI так, что сообщество будет само писать код, а ваши доверенные лица будут ревьюить, общий workflow будет соблюден.

Вы даже можете в coveralls / scrutinizer настроить правила, чтобы Check Suite падал если % покрытия кода меньше 100%, а в Readme напичкать баджиками для красоты, например так:

Буду рад, если мой туториал будет кому-то полезен, т.к. перед написанием данного поста я впервые столкнулся с GitHub Actions, я не DevOps и настройкой CI не занимаюсь, самому пришлось прогуглить не один сайт, чтобы настроить такой workflow, который был нужен мне.

Подробнее..

Настраиваем GitHub Actions для Android с последующим деплоем в PlayMarket

15.06.2020 12:08:41 | Автор: admin
Привет, Хаброжители! На днях начал изучать GitHub Actions для Android. Ранее у меня была удачная попытка настройки данного функционала для проекта на Flutter, но без деплоя, для которого полно информации и гайдов как на англоязычных ресурсах, так и на русскоязычных, а вот с нативным андроидом не всё так прозаично. И решил записать основные проблемы и их решение.

Этап первый: настройка для автоматической подписи готового apk


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

Я использую вариант с использование файла keystore.properties, который позволяет нам добавить ключ разработчика в папку проекта, не светя при этом паролями от него, делается это так:

apply plugin: ...def keystorePropertiesFile = rootProject.file("keystore.properties")def keystoreProperties = new Properties()keystoreProperties.load(new FileInputStream(keystorePropertiesFile))android {  ...  signingConfigs {    release {        storeFile file("../MyKey.jks")        storePassword keystoreProperties['RELEASE_STORE_PASSWORD']        keyAlias keystoreProperties['RELEASE_KEY_ALIAS']        keyPassword keystoreProperties['RELEASE_KEY_PASSWORD']    }    debug {        storeFile file('../debug.keystore')    }  }  buildTypes {    release {        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'        minifyEnabled false        signingConfig signingConfigs.release        buildConfigField "String", "PIN_ALIAS", keystoreProperties['PIN_ALIAS']        buildConfigField "String", "DB_PASS_ALIAS", keystoreProperties['DB_PASS_ALIAS']    }    debug {        minifyEnabled false        signingConfig signingConfigs.debug        buildConfigField "String", "PIN_ALIAS", keystoreProperties['PIN_ALIAS']        buildConfigField "String", "DB_PASS_ALIAS", keystoreProperties['DB_PASS_ALIAS']    }  }}dependencies {  ...}

И тут возникла проблема, как сделать так, что бы мы могли взять ключи из ${{ secrets.MY_KEY }} и при этом градл понимал, если у нас есть keystore.properties, то берём из него, если нет то берём из секретов? Решение нашлось на одном из гайдов для флаттера, где для этого они использую окружения (Кстати, здесь классный подход, чтобы не светить нашим ключём разработчика), но проблему это не решило. Перепробовав несколько вариантов с введением дополнительных файлов и т.п., остановился на самом простом: мы вводим дополнительно несколько переменных(в зависимости от нужного нам количества), и проверяем наличие файла keystore.properties:

def release_store_passworddef release_key_passworddef release_key_aliasdef pin_aliasdef db_pass_aliasdef keystoreProperties = new Properties()if (rootProject.file("keystore.properties").exists()) {    keystoreProperties.load(new FileInputStream(rootProject.file("keystore.properties")))    release_store_password = keystoreProperties['RELEASE_STORE_PASSWORD']    release_key_password = keystoreProperties['RELEASE_KEY_PASSWORD']    release_key_alias = keystoreProperties['RELEASE_KEY_ALIAS']    pin_alias = keystoreProperties['PIN_ALIAS']    db_pass_alias = keystoreProperties['DB_PASS_ALIAS']} else {    release_store_password = System.env.RELEASE_STORE_PASSWORD    release_key_password = System.env.RELEASE_KEY_PASSWORD    release_key_alias = System.env.RELEASE_KEY_ALIAS    pin_alias = System.env.PIN_ALIAS    db_pass_alias = System.env.DB_PASS_ALIAS}android {   signingConfigs {        release {            storeFile file("../my_key.jks")            storePassword = release_store_password            keyAlias = release_key_alias            keyPassword = release_key_password        }    buildType{       release {          buildConfigField "String", "PIN_ALIAS", "\"$pin_alias\"" //если вам нужно ввести некоторые           buildConfigField "String", "DB_PASS_ALIAS", "\"$db_pass_alias\"" // дополнительные данны.      }    }}

Итак, теперь наш сборщик умеет собирать и сразу подписывать наш apk.

Этап второй: версия сборки.


Тут нет ничего сверх естественного, хотелось получить какой-то, достаточно универсальный вариант, минимальной сложности. Погуглив, присмотрелся, сколько разработчиков столько и вариантов и каждый извращается как может. Мне какие-то сверх сложные подходы не нужны и я уже хотел было использовать BUILD_NUMBER, но тут я наткнулся на параметр у для GitHub actions: ${{ github.run_number }}.

${{ github.run_number }}
Уникальный номер для каждого запуска определенного рабочего процесса в хранилище. Это число начинается с 1 для первого запуска рабочего процесса и увеличивается с каждым новым запуском. Это число не изменится, если вы повторно запустите рабочий процесс. (Запуском здесь подразумевается когда вы пушите в ветку).

По этому взвесив все за и против имеем следующее решение:

def versionPropsFile = rootProject.file('version.properties')Properties versionProps = new Properties()versionProps.load(new FileInputStream(versionPropsFile))def verCode = versionProps['VERSION_CODE'].toInteger()android {  defaultConfig {    versionCode verCode    versionName "1.1.$verCode"  }}//version.properties файлVERSION_CODE=1

В рабочем процессе делаем так:

- name: Output version code        run: echo VERSION_CODE=${{ github.run_number }} > ./version.properties

Этап третий: развертывание (deploy)


На данный момент я нашел два готовых решения: Gradle Play Publisher и Upload Android release to the Play Store

Первый вариант отпал по причине: использование гаубицы при стрельбе по воробьям. По этому я выбрал второй. Ничего вроде сложного в нём нет, а тут есть подробная инструкция: Тут.

- name: Upload to PlayMarket        uses: r0adkll/upload-google-play@v1        with:          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}          packageName: com.guralnya.notification_tracker          releaseFile: app/build/outputs/apk/release/notification_tracker.release.apk          track: beta          userFraction: 0.33          whatsNewDirectory: distribution/whatsnew

Но некоторые моменты у меня всё же возникли:

  • serviceAccountJson и serviceAccountJsonPlainText с первым я так и не разобрался в каком виде его нужно положить в секреты, второй же просто берём содержимое файла и кладём в наш секрет.
  • releaseFile использовал самый простой подход, когда мы берём готовый файл из папки с проектом, но вариант со звёздочкой не прокатил: notification_tracker.release.*.apk, где у меня стоит время сборки. Хотя в другом экшене, который у меня используется для загрузки файла (actions/upload-artifact@v2), такой подход работал отлично.
  • whatsNewDirectory внимательнее к языковым кодам. Если английский я взял из гугл-консоли при добавлении новой версии (en-IN), а Русский как (ru-RU), то логично предположить что все языки работают по том же принципу, но нет Украинский я не доглядел, а он там помечен как (uk), потому если не хотите лишний раз комититься и видеть красный крестик, лучше свериться с той же консолью.

Есть ситуация, когда у вас может по какой-то причине не деплоиться, это когда вы ещё не опубликовали приложение (встречать не приходилось, но в одной статье было описано). Так что начинайте с того, что сначала опубликуйте приложение в ручную если вы ещё этого не сделали.

Итоговый рабочий процесс будет оптимизироваться и улучшаться вместе с файлом градла


Android CI.yaml:

name: Android_CIon:  push:    branches:      - beta_releasejobs:  build:    runs-on: ubuntu-latest    name: Build release-apk and deploy to PlayMarket    steps:      - uses: actions/checkout@v2      - name: set up JDK 1.8        uses: actions/setup-java@v1        with:          java-version: 1.8      # Without NDK not compile and not normal error message. NDK is required      - name: Install NDK        run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}      # Some times is have problems with permissions for ./gradle file. Then uncommit it code      #    - name: Make gradlew executable      #      run: chmod +x ./gradlew      - name: Output version code        run: echo VERSION_CODE=${{ github.run_number }} > ./version.properties      - name: Build with Gradle        run: ./gradlew assemble        env:          RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}          RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}          RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}          PIN_ALIAS: ${{ secrets.PIN_ALIAS }}          DB_PASS_ALIAS: ${{ secrets.DB_PASS_ALIAS }}      - name: Upload APK        uses: actions/upload-artifact@v2        with:          name: notification_tracker          path: app/build/outputs/apk/release/notification_tracker.release.apk      - name: Upload to PlayMarket        uses: r0adkll/upload-google-play@v1        with:          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}          packageName: com.guralnya.notification_tracker          releaseFile: app/build/outputs/apk/release/notification_tracker.release.apk          track: beta          userFraction: 0.33          whatsNewDirectory: distribution/whatsnew

Важный момент


необходимость NDK. Без установленного NDK у вас не соберётся проект, по крайней мере релизный. Можно долго гадать в чём проблема и искать решение, так как нормального сообщения ошибки нет. Иногда можно отловить вот это: Task :app:stripDebugDebugSymbols FAILED. После гуглинга и экспериментов, оказалось что нет NDK. Делаем так:

- name: Install NDK        run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}

P.S. Для Gradle использовал подсветку кода от Kotlin. Для YAML от JSON. Конечно немного не то, но лучше мне найти не удалось, если есть лучшие варианты, сообщите мне пожалуйста и я исправлю.

P.S.S. Может быть у кого есть лучшее решение или предложения по улучшения, напишите их в комментариях, так как по первому этапу вопрос провисел на StackOverflow больше 10-ти дней, но ответа так и не последовало.
Подробнее..

Прокачиваем Android проект с GitHub Actions. Часть 1

03.12.2020 10:23:07 | Автор: admin

Привет!

Это пост для тех, кто заинтересовался возможностями GitHub Actions, но никогда не имел опыта реальной настройки build-систем. Примеры будут полезны как для прокачки собственного pet-проекта, так и для понимания, как настраивается CI/CD, если по работе нет связанных с этим задач.

Что будет рассмотрено:

  • Основные понятия для построения CI/CD на GitHub Actions.

  • Настроим работающий workflow который запускает Unit-тесты при создании pull request.

  • Добавим бейджики со статусом созданных workflow в репозиторий.

  • Настроим работающий workflow для сборки релизных артефактов APK и AAB.

  • Научимся безопасно подписывать ключом релизный APK.

GitHub Actions был выбран для примеров, потому что позволяет не углубляясь в инфраструктурные сложности с развёртыванием своего собственного CI-сервера буквально за день собрать работающий пайплайн для прогона тестов, подписи приложения и даже загрузки в Google Play. Кроме того, у GitHub Actions полная интеграция с GitHub, очень легко взаимодействовать с репозиторием. Для открытых репозиториев услуга бесплатная, для закрытых предусмотрены разные тарифные планы.

Но главное преимущество GitHub Actions состоит в возможности переиспользовать готовые блоки бизнес-логики (actions), причём не только свои собственные. На большинство самых распространённых задач уже скорее всего есть свой Action, который вы можете включить в свой пайплайн! Какие экшены уже написаны участниками сообщества, можно посмотреть наhttps://github.com/marketplace?type=actions

Примеры будут настраиваться на самом простом проекте с одной пустой Activity из шаблонов Android Studio и на новом пустом репозитории в GitHub.

Общие слова про Github Actions

Если кто-то представляет себе, как собирают автомобили на заводах, это неплохая иллюстрация к тому, чем вообще занимается CI/CD.

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

На вход конвейера попадает коммит в репозиторий или пулл-реквест.Потом код попадает на участок сборки приложения, далеезапускаются unit и UI-тесты. Если тесты прошли успешно, можно смело двигаться дальше по конвейеру, например, выложить в раздел релизов артефакт для истории версий.

Основные понятия

Вот так по блокам можно представить, как структурирован workflow в Github Actions.

Runner

Это развёрнутый в облаке от GitHub или self-hosted сервер с настроенным окружением икоторый может запускать workflow внутри себя.

Workflow

Это независимый процесс, автоматически запускаемый на GitHub Actions в отдельном контейнере по получению Event. Каждый workflow описывается отдельным YAML-файлом.

Состоит из более мелких структурных единиц исполнения - Jobs.

Job

Составная часть workflow, в свою очередь состоит из отдельных шагов Steps. Jobs могут быть настроены на параллельное и последовательное выполнение.

Step

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

Actions

Самая маленькая структурная единица исполнения скрипта workflow. Action может делать в принципе всё что угодно, например, проставлять теги с версией приложения в Git или отправлять собранный AAB в Google Play.

Можно писать как собственный Action, так и пользоваться готовыми. Action по сути выступает наравне с другими командами внутри Step.

Самые распространённые Action - это checkout на коммит и установка Java-окружения. По умолчанию, если специально не встать на нужный коммит, Job ничего не знает о проекте, из которого он запущен.

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

- uses: actions/checkout@v1- uses: actions/setup-java@v1

Event

Внутренние или внешние события, которые запускают workflow. Commit, pull request, comment, tag - все эти события могут быть использованы в ваших скриптах как триггер для старта каких-то действий. Еще workflow может быть настроен на ручной запуск (https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/) и запуск по cron расписанию (https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#scheduled-events)

Hello, world!

Всё, что связано в GitHub Actions, располагается на вкладке Actions в репозитории.

При создании нового workflow GitHub пытается проанализировать содержимое репозитория и предлагает шаблон на выбор. На самые популярные сценарии сборки и деплоя можно найти заготовки.

Все workflow конфигурируются через файлы в формате YAML, это фактически стандарт для CI/CD-систем.

Чтобы GitHub Actions начала выполнять таски, необходимо положить их в определённым образом названную директорию в корне проекта github/workflows.

Добавлять и редактировать конфиги можно как в Android Studio, так и в самом GitHub на вкладке Actions. Так и поступим.

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

GitHub сразу же подставляет самый простой скрипт, который делает checkout на новый коммит, выводит в консоль несколько строк и заканчивает работу. Что-то ещё проще придумать сложно, но даже в этом примере есть что посмотреть.

# This is a basic workflow to help you get started with Actionsname: CI# Controls when the action will run. Triggers the workflow on push or pull request # events but only for the develop branch on: push: branches: [ main ] pull_request: branches: [ main ]# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest# Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2# Runs a single command using the runners shell - name: Run a one-line script run: echo Hello, world!# Runs a set of commands using the runners shell - name: Run a multi-line script run: | echo Add other actions to build, echo test, and deploy your project.

Сделать коммит с новым скриптом можно прямо из веб-интерфейса GitHub.

Но когда этот workflow будет отрабатывать? Ведь мы раньше упоминали, что в GitHub Actions все workflow запускаются не сами по себе, а только при получении того eventа, который прописан в самом yml-скрипте.

on: push: branches: [ main ] pull_request: branches: [ main ]

Тут видно, что наш тестовый скрипт будет выполняться для любых коммитов и пулл-реквестов в ветку main.

Стойте, так мы ведь только что сделали коммит с новым hello_world.yml, получается, он уже должен был сработать? Совершенно верно, можно прямо сейчас зайти в раздел actions и посмотреть результат работы скрипта.

Уже неплохо! Обычно после первого знакомства с новой технологией сразу хочется усложнить свой hello world и заставить его делать хоть что-то полезное, кроме вывода текста в консоль.

Запуск unit-тестов на каждый pull request в main

Первый YAML-скрипт мы создавали в веб-интерфейсе GitHub, теперь сделаем то же самое в Android Studio.

Чтобы увидеть директорию с YAML-файлами, нужно переключить режим просмотра на Project (если вдруг у вас был выбран режим Android).

Находим директорию workflows и создаём новый файл с типом YML. Назовём его, к примеру, run_unit_tests.yml.

Пока что всё, что мы хотим от скрипта, - это запускать unit-тесты на каждом pull request в ветку main. Можно скопировать целиком код из примера, всё должно работать. Если GitHub покажет, что в YAML ошибка, то проверить в первую очередь стоит правильность форматирования и количество отступов у блоков, так как формат чувствителен к этому.

name: PR_unit_testson:  pull_request:    branches:  - 'main'jobs:  Unit-test:  name: Run unit tests on PR in main  runs-on: ubuntu-20.04  steps:    - uses: actions/checkout@v2    - uses: actions/setup-java@v1      with: {java-version: 1.8}    - name: Run unit tests      run: ./gradlew test

actions/checkout@v2иactions/setup-java@v1подготавливают окружение для запуска тестов, первый выкачивает репозиторий и встаёт на нужный коммит, а второй устанавливает Java 8 - окружение. Это те самые Actions, которые даже упоминаются в названии, самые маленькие исполняемые единицы workflow. Можно рассматривать их как подключаемые к вашему workflow внешние библиотеки. Если интересно, что именно делают эти Actions, переходите по ссылкамhttps://github.com/actions/checkouthttps://github.com/actions/setup-java

run: ./gradlew test запускает тесты с помощью gradle wrapper.

Запускать можно всё то же самое, что и в консоли, доступны все команды shell. Можно ещё написать свой собственный shell-скрипт и просто запустить его в этом месте, например, так: run: ./run_unit_tests.sh

Тут открывается простор для автоматизации всего что только можно. Если раньше вы никогда самостоятельно не писали shell-скрипты, рекомендую прочитать книгу The Linux Command Line: a Complete introduction от William Shotts, оченьхорошеевведение в shell-автоматизацию.

Готово! Создаем любой пулл-реквест в ветку main и смотрим во вкладке Actions, что получилось.

Я специально испортил один unit-тест, чтобы показать ещё одну базовую настройку вашего CI/CD-пайплайна - запрет на merge в ветку main c поломанными тестами. Всё логично: если ваш новый коммит что-то поломает в бизнес-логике приложения, то автоматика не даст сделать по ошибке merge. Или по крайней мере предупредит о проблеме.

Настраивается это очень просто: заходим в Settings репозитория, вводим в Branch name pattern паттерн для тех веток, для которых хотим создать новое правило безопасности. В нашем случае можно ввести main. Далее проставляем галочки в нужных условиях правила и сохраняем.

Всё готово, вы только что создали своё первое правило для merge в своем репозитории.Смотрим теперь, как поведёт себя автоматика с pull-request, в котором поломаны тесты.

Работает! При желании можно запретить merge с проблемами даже для администраторов, там же в настройках merge protection rule.

Если хочется прямо сейчас самостоятельно что-то настроить, то вот несложное задание. Gradle task test, который мы запускали, генерит небольшой отчет по результатам запуска unit-тестов, всё лежит в app/build/reports/tests/testDebugUnitTest/

Попробуйте самостоятельно добавить после шагаRun unit tests ещё один шаг, который выкачивает отчет по тестированию.

Подсказка - использоватьactions/upload-artifact@v2

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

Задачу сформулируем так:подготавливать нам APK и AAB и подписывать ключом из keystore. Причём сборку мы будем запускать только на pull request в main из веток с именем, начинающимся сrelease/

Задача стала чуть сложнее, поэтому будем рассматривать ее по шагам.

Шаг 1. Собираем APK и AAB. Пока не подписываем

name: Test_and_build_artifacts_on_releaseon: pull_request:   branches:     - 'main'jobs: build_apk_aab:   if: startsWith(github.head_ref, 'release/') == true   name: Build release artifacts   runs-on: ubuntu-20.04   steps:     - uses: actions/checkout@v2     - uses: actions/setup-java@v1       with: {java-version: 1.8}     - name: Build release APK and AAB after test       run: |         ./gradlew test         ./gradlew assembleRelease         ./gradlew bundleRelease     - name: Upload APK       uses: actions/upload-artifact@v2       with:         name: app-release.apk         path: app/build/outputs/apk/release/app-release-unsigned.apk     - name: Upload AAB Bundle       uses: actions/upload-artifact@v2       with:         name: app-release.aab         path: app/build/outputs/bundle/release/app-release.aab

Вот эта строчка является проверкой имени ветки, из которой создается pull request, и, если условие выполняется, workflow продолжается. Мы ведь решили запускать сборку и подпись только для релизных веток.

if: startsWith(github.head_ref, 'release/') == true

Этот блок команд запускает, используя Gradle wrapper, тесты, а затем сборку APK и AAB. Обратите внимание, вертикальная черта позволяет запускать несколько shell-команд в одном блоке run.

run: |  ./gradlew test  ./gradlew assembleRelease --stacktrace  ./gradlew bundleRelease

Следующий шаг достанет после сборки APK и оставит его в виде артефакта в GItHub. Если этого не сделать, все временные файлы будут удалены после завершения workflow. Стоит обратить внимание, что APK остаётся неподписанным, мы просто не сконфигурировали пока ничего для этого. В таком виде APK его ещё нельзя выложить в Google Play, как настроить автоматическое подписание, будет рассказано дальше.

Подробнее про Actionupload-artifact@v2 можно посмотреть тут. Основное, что может этот Action, - это выкачать файл по имени либо целиком директорию и упаковать в zip-архив.

- name: Upload APK  uses: actions/upload-artifact@v2  with:    name: app-release.apk    path: app/build/outputs/apk/release/app-release-unsigned.apk

Аналогичным образом достаем и AAB-файл.

Шаг 2. Подписываем APK

Сначала немного теории, как и зачем вообще подписывать APK.

Цифровая подпись необходима для того, чтобы Google Play мог идентифицировать разработчика и в дальнейшем только он мог обновлять приложение, это крайне важная вещь в процессе размещения проекта в магазине приложений.

В целях безопасности цифровая подпись хранится не в открытом виде, а в специальном хранилище типа key value - файле с расширением jks или keystore. Сам файл хранилища стоит держать в надёжном месте, это, можно сказать, паспорт вашего приложения.

Как создать keystore

Если вы уже выложили своё приложение в Google Play, то ключ у вас точно есть. Если же нет - ниже простая инструкция.

Вариантов два - создать через консоль или через IDE.

  1. Консоль

$ keytool -genkey -v -keystore my_app_keystore.keystore -alias app_sign_key -keyalg RSA -keysize 2048 -validity 10000

my_app_keystore.keystore- это название самого хранилища, которое мы создаем.

app_sign_key- название ключа, по которому мы будем доставать наш секретный ключ.

10000- время жизни ключа в днях (примерно 27 лет).

Вводим пароль на хранилище, пароль на ключ и потом по желанию метаданные о владельце. Всё, хранилище готово, можно пользоваться.

2)В Android Studio

В меню студии заходим в Build -> Generate Signed Bundle / APK.

Дальше Next -> Create New и вводим всё то же самое: пароли, имя хранилища и имя ключа в хранилище.

Чтобы собранный APK успешно подписать ключом из хранилища, необходимо этот самый ключ достать по имени (key alias), предварительно получив доступ через пароль к хранилищу (store password) и пароль непосредственно к ключу (key password). Это происходит в рамках специального Gradle task, всё будет далее автоматизировано.

Дополнительная информация

https://developer.android.com/studio/build/building-cmdline#gradle_signing

https://developer.android.com/studio/publish/app-signing#secure-shared-keystore

https://developer.android.com/studio/publish/app-signing#sign-auto

И тут возникает два вопроса.

  1. Где хранить пароли от хранилища, не в открытом же виде прописывать их в конфигах?

  2. Как и куда выкладывать само хранилище ключей для открытого проекта?

Для хранения секретных данных, например, таких как идентификаторы приложения в Facebook, VK или Firebase, сервис GitHub предлагает механизмSecrets.

Особенность механизма хранения в том, что после сохранения ключа уже невозможно будет просто так зайти и посмотреть его. Только ввести новый, полностью удалить или использовать как естьв коде по имени. В логах секретные данные скрываются за звёздочками.

После добавления секретов к ним можно обращаться через специальный синтаксис прямо из YAML-скриптов. Например, вот так мы запишем EXAMPLE_API_KEY_1 в переменную окружения и затем в Gradle-скрипте, которому она понадобится, достанем её через System.getenv('EXAMPLE_API_KEY_1')

env:  API_KEY: ${{ secrets.EXAMPLE_API_KEY }}

Отлично, часть проблемы решена, но куда положить само хранилище?

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

Ничего кроме хранилища мы не собираемся помещать в новый приватный репозиторий. Мы будем клонировать его прямо в наш основной репозиторий в директорию app/keystore перед подписью APK-файла и доставать из него ключ с помощью паролей, который поместим в секцию Secrets в основном репозитории. Вот так будет выглядеть структура проекта на CI после клонирования репозитория с ключом в проект с основным проектом.

Звучит не очень сложно, смотрим, как такое настроить в GitHub Actions.

  1. Создаемприватныйрепозиторий и помещаем туда только хранилище ключей.

  2. Генерируем Personal access token для доступа к приватному репозиторию с хранилищем.

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

  3. Добавляем Personal access token из предыдущего шага в секреты основного проекта под любым именем, например KEYSTORE_ACCESS_TOKEN.

  4. Добавляем все пароли и key_alias от хранилища.

    Добавляем название аккаунта и имя приватного репозитория туда же в секреты основного проекта через слеш, что-то вроде another-account/secret-repo. Это понадобится нам дальше, когда будем клонировать репозиторий с ключом в YAML-скрипте.

  5. Оформляем workflow для сборки APK и AAB в YAML-файле.

name: Test_and_build_signed_artifacts_on_releaseon:  pull_request:    branches:      - 'main'env:  KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}  RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}  RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}jobs:  build_apk_aab:    if: startsWith(github.head_ref, 'release/') == true    name: Build release artifacts    runs-on: ubuntu-20.04    steps:      - uses: actions/checkout@v2      - uses: actions/setup-java@v1        with: {java-version: 1.8}      - name: Checkout keystore repo        uses: actions/checkout@v2        with:          repository: ${{ secrets.KEYSTORE_GIT_REPOSITORY }}          token: ${{ secrets.KEYSTORE_ACCESS_TOKEN }}          path: app/keystore      - name: Run tests and build release artifacts        run: |          ./gradlew test          ./gradlew assembleRelease --stacktrace          ./gradlew bundleRelease      - name: Upload signed APK        uses: actions/upload-artifact@v2        with:          name: app-release.apk          path: app/build/outputs/apk/release/app-release.apk      - name: Upload AAB Bundle        uses: actions/upload-artifact@v2        with:          name: app-release.aab          path: app/build/outputs/bundle/release/app-release.aab

За основу был взят workflow, который был описан ранее. Запускается так же на pull request в main, только из веток, начинающихся на release/*. Вы можете поменять так, как вам удобно, это просто для иллюстрации возможностей.

Что тут добавилось? Во-первых, в начале workflow записываются переменные окружения, вот тут:

env:  KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}  RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}  RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}

Далее последовательно делаем два checkout - сначала на коммит в созданном pull request (это было и раньше), потом делаем checkout приватного репозитория с хранилищем.

- name: Checkout keystore repo  uses: actions/checkout@v2  with:    repository: ${{ secrets.KEYSTORE_GIT_REPOSITORY }}    token: ${{ secrets.KEYSTORE_ACCESS_TOKEN }}    path: app/keystore

Тут уже есть особенности. Необходимо передать в checkout@v2 аргумент, в какой репозиторий стучаться (repository), токен для доступа к нему (token) и path. Path - это путь внутри директории с основным проектом, куда нужно сложить файлы. Мы хотим получить хранилище в app/keystore. В принципе, не обязательно именно такой путь, главное указать выбранный путь в Gradle, чтобы он понимал, где искать хранилище. Полную документацию по checkout@v2 можно почитать тут.

Дальше всё уже знакомое. Запускаем тесты и сборку релизной версии артефактов. На этом с workflow всё, дальше начинаем подготавливать build.gradle проекта.

Редактируем build.gradle

signingConfigs {   release {       def keystoreProperties = new Properties()       def keystorePropsFile = file("keystore/keystore_config")       if (keystorePropsFile.exists()) {           file("keystore/keystore_config").withInputStream { keystoreProperties.load(it) }           storeFile file("$keystoreProperties.storeFile")           storePassword "$keystoreProperties.storePassword"           keyAlias "$keystoreProperties.keyAlias"           keyPassword "$keystoreProperties.keyPassword"       } else {           storeFile file("keystore/my_app_keystore")           storePassword System.getenv('KEYSTORE_PASSWORD')           keyAlias System.getenv('RELEASE_SIGN_KEY_ALIAS')           keyPassword System.getenv('RELEASE_SIGN_KEY_PASSWORD')       }   }}buildTypes {   release {       signingConfig signingConfigs.release       minifyEnabled false       proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'   }}

Идея заключается в том, что в заранее указанную директорию (app/keystore/) на CI автоматически добавится хранилище, а у себя локально мы можем без опаски хранить его в структуре проекта и даже положить туда файл с паролем в открытом виде. Это если нам хочется собирать и подписывать APK локально.

Главное при этом добавить в gitignore всё содержимое app/keystore, чтобы случайно секретная информация не утекла с очередным коммитом.

*.iml.gradle/local.properties/.idea/caches/.idea/libraries/.idea/modules.xml/.idea/workspace.xml/.idea/navEditor.xml/.idea/assetWizardSettings.xml.DS_Store/build/captures.externalNativeBuild.cxxlocal.properties/keystore # <-- вот эту строчку мы добавили

Чтобы Gradle понимал, где ему брать my_app_keystore в случае запуска assembleRelease локально и на CI, делаем нехитрую проверку. Сначала ищем keystore_config в директории keystore. Не нашли - делаем вывод, что нас запустили на CI и пароль следует брать не из keystore_config-файла, а из переменных окружения.

keystore_config - тут стандартный способ хранить в открытом виде пароли, внутри он состоит из пар key=value. Всё то же самое, что мы записывали в секреты на GitHub, но в открытом виде.

storeFile=keystore/my_app_keystorestorePassword=654321keyAlias=sign_apk_keykeyPassword=123456

Само зашифрованное хранилище кладём рядом, в той же директории.

Если потребности подписывать APK локально нет или хочется вручную запускать процесс через Generate Signed Bundle / APK, выбирая каждый раз нужный keystore, то можно всё упростить и оставить только часть про System.getenv()

Тут самостоятельно примите решение, что вам подходит, безопасное хранение паролей - действительно важная история.

Пробуем запустить на CI

Отлично! То, что нам нужно, - готовый к релизу артефакт, собранный автоматически на GitHub Actions.

Запускаем локально.

В Android Studio переходим в терминал, запускаем.

./gradlew assembleRelease

После успешного завершения подписанный APK будет ждать нас в app/build/outputs/apk/release.

На этом со сборкой артефактов можно закончить, самые базовые кейсы рассмотрены.

Чтобы потренироваться с этим, предлагаю вам самостоятельно настроить подпись для debug-сборок.

Иеще одно самостоятельное задание для заинтересовавшихся: чтобы сделать проект еще красивее, по проставлению в git tag версии приложения (например, v1.0.0) собирать APK, подписывать и складывать в разделе релизов прямо в GitHub репозитории с правильным названием, включающим версию из tag.

Подсказка - удобно использоватьhttps://github.com/actions/create-release, там в описании есть похожий на нашу задачу workflow.

Выводим на README.MD статус выполнения workflow

Здорово смотрятся бейджи со статусом прохождения этапов CI на главной странице репозитория. Наверняка вы много раз видели похожие бейджики с процентом покрытия тестами кода, статусом сборки и так далее. Давайте прямо сейчас сделаем такие для статуса прохождения unit-тестов, для этого у нас уже всё есть.

Итак, у нас уже есть несколько workflow. Документацияочень подробная и с примерами.

ОткрываемREADME.mdи пишем что-то вроде этого, подставляя своё реальное имя на GitHub, название текущего репозитория и имя workflow, для которого хочется иметь бейджик.

Feature branch Unit tests status![PR_unit_tests](http://personeltest.ru/aways/github.com/{your_github_acc_name}/{repository_name}/workflows/PR_unit_tests/badge.svg)Main branch status![main](http://personeltest.ru/aways/github.com/{your_github_acc_name}/{repository_name}/workflows/Hello_world/badge.svg)

Сохраняем и смотрим результат.

По-моему, круто всего для двух минут настройки. Теперь всегда будет видно текущий статус прогона тестов. А ещё можно будет добавить статус сборки APK для релизных веток, что-нибудь от статического анализатора кода вроде Sonarcube, в общем, всё, что пожелаете.

На этом первая часть рассказа про GitHub Actions заканчивается.За короткое время мы смогли настроить очень неплохую автоматизацию для проекта.

В следующей части продолжим тему тестирования и посмотрим как настроить запуск UI тестов в Firebase Test Lab. Не пропустите, будет интересно.

Подробнее..

Статьи это тоже исходный код

17.12.2020 12:23:08 | Автор: admin

Title


Открываю VS Code и начинаю набирать статью с самого начала. Но вот незадача формат маркдауна не совсем совместим с имеющимся форматом Хабра. Получается выхода нет и придётся возвращаться к встроенному редактору Хабра;


Или не придется?

В голову пришла идея написать утилиту, которая конвертирует разные форматы маркдаунов друг в друга, например, из формата GitHub в формат Habr;


Такую программу я в итоге и разработал. Теперь не надо копировать статьи в редактор хабра, чтобы посмотреть как она выглядит, можно продолжать писать в любимом VS Code;


Хотя я и использую множество плагинов VS Code, но мысли о неэффективном процессе написания статей не исчезли. Раз уж я набираю текст в VS Code, то почему бы сразу не делать коммиты контента в гит-репозиторий?


Это дало бы немало новых возможностей, которыми пользуются программисты: версионирование, бекапы на локальные носители или веб-сервисы, правки от редакторов и пользователей. А еще можно внедрить CD/CI;


В итоге, я написал небольшой гайд для разработчиков, как писать техническую документацию в редакторах, используя мою утилиту. Саму утилиту можно посмотреть в моём репозитории на GitHub;





Пишем {



Написание статьи начинается с выбора текстового редактора. Существует большое количество специальных текстовых редакторов и всех их рассмотреть очень трудно, поэтому я остановился на наиболее популярных и разнородных;


Visual Studio Code


Возможности по написанию статей внушительны, тут и поддержка Markdown и разнообразие плагинов под любую задачу. Также есть подсветка синтаксиса кода;


Интерфейс VS Code удобно разделён на окна: слева отображается код текста, а справа визуализация кода, работающая в реальном времени;


О VS Code написано много статей и гайдов, в том числе и на хабре, поэтому не буду много расписывать о возможностях редактора, а перечислю плагины, которые оказались очень полезными для меня:


  • Проверка синтаксиса
  • Проверка форматирования
  • Форматирование таблиц
  • Генерация оглавления

Проверка синтаксиса


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


В VS Code, для проверки синтаксиса, я использую Code Spell Checker (русский словарь необходимо устанавливать отдельно), и Grammarly;


Проверка форматирования


В данной статье текст это исходный код, а значит к нему применимы принципы форматирования. Расширение markdownlint проверяет код на соответствие стандартным правилам, с которыми можно ознакомиться на странице репозитория этого расширения;


Форматирование таблиц


Table Formatter


В Markdown громоздкий и не особо удобный синтаксис для описания таблиц. К счастью, существуют различные расширения, которые позволяют улучшить процесс создания и форматирования таблиц, например, расширение Table Formatter;


Генерация оглавления


Markdown TOC


Иногда возникает необходимость сгенерировать оглавление (Table of Content, TOC) для всего документа на основе заголовков, причем чтобы оглавление автоматически обновлялось. Для этого существует несколько расширений, но мне нравится плагин Markdown TOC. Данный плагин я использовал для генерации оглавления в этой статье;


Typora


Typora возможно понравится любителям визуального программирования, так как совмещает в себе плюсы WYSIWYG и текстовых редакторов и позволяет сразу увидеть финальное представление текста в документе. Typora поддерживает Markdown;


Typora


Веб-интерфейс как редактор


Можно писать статьи сразу в веб-браузере, используя, например, хабр, GitHub, stackedit.io, dillinger.io и другие сервисы. Хотя это не так удобно и не особо вписывается в процессы разработчика, зато дает возможность набирать текст хоть на планшете или телефоне;


Pandoc, как универсальный конвертер


Pandoc универсальная утилита, позволяющая конвертировать одни текстовые файловые форматы в другие. Можно использовать word или latex как основной текстовый редактор и в дальнейшем, с помощью пандока, конвертить один формат в другой. Pandoc поддерживает огромное количество форматов, например, md, docx, txt, rtf, pdf, html и другие. При этом, пандок очень прост в использовании и позволяет форматировать один файл в другой с помощью одной команды, например:


pandoc text.docx -o text.md

Также есть веб-версия, позволяющая воспользоваться возможностью конвертации в режиме онлайн;


}


Конвертируем {



После того как статья написана, её нужно сконвертировать в Markdown-формат хабра. Этого можно было бы избежать, если бы markdown-синтаксис хабра полностью сочетался с GitHub Flavored Markdown, который реализуется во многих других редакторах, в частности, в VS Code;


Однако существует несколько несоответствий, самое важное из которых неправильная обработка переносов строк. Из-за этого приходится использовать однострочные абзацы, что плохо сочетается с системами контроля версий diff раздувается и становится малоинформативным;


Есть и другие различия: реализация спойлеров, внутренних ссылок и.т.д;


Используем MarkConv


Конвертировать правильно можно с помощью утилиты MarkConv следующим образом:


  1. Устанавливаем .NET Core (если его еще нет).


  2. Устанавливаем .NET Tool следующим образом:


    dotnet tool install -g MarkConv.Cli --version 1.0.0 --add-source https://www.myget.org/F/mark-conv/api/v3/index.json
    

  3. Конвертируем статью в формат маркдаун-синтаксиса хабра следующим образом:


    markconv -f <article.md> -o Habr
    


Утилиту можно запускать и в batch-режиме из папки с md-файлами, что позволит запустить конвертацию сразу нескольких файлов одновременно;


По завершению процесса конвертирования статью можно публиковать на хабр;


Стоит отметить, что MarkConv конвертирует не только в формат хабра, но и в формат dev.to, а также делает другие полезные проверки и замены;


Проверка синтаксиса с помощью MarkConv


В тексте в формате Markdown нет формальных ошибок уместна любая последовательность символов. Это и достоинство и недостаток текст всегда можно прочитать, но какие-то вещи могут ненамеренно отображаться некорректно.


Однако в таком тексте могут использоваться HTML-секции, в которых могут быть ошибки типа незакрытых тегов. В своих статьях я сразу нашел несколько, например:


[INFO] Converting of file /home/appveyor/projects/articles/Modern-Presentations-Format/Russian.md...[WARN] Incorrect nesting: element </href> at [358,162..166) closes <a> at [358,14..15)

Ограничение на размер текста относительно тега cut


На хабре есть резонные ограничения на размер текста до и после использования тега cut. MarkConv проверяет эти правила и выводит ошибку при нарушении, например:


You need to insert <cut/> tag if the text contains more than 1000 characters

Проверка абсолютных ссылок


Использование ключа --checklinks позволит запустить утилиту MarkConv на проверку всех ссылкок вида http://;


На данный момент это работает с некоторой задержкой и не всегда корректно, возможно таким образом на внешних сервисах реализована защита от DDoS-атак;


Замена отображения локальных адресов ресурсов на абсолютные


Чтобы ресурсы не зависили от внешних сервисов (например, картинки), то имеет смысл использовать тег linkmap следующим образом:


<linkmap src=Markdown.svg dst=https://habrastorage.org/getpro/habr/post_images/a40/f88/64c/a40f8864c5f8db7888076cf30f5411f5.svg />

где: src адрес локального ресурса, а dst удалённого.


При этом картинку нужно еще дублировать на сторонний сервис. Таким образом, статья всегда будет правильно открываться локально, даже если сервер с картинками упал, в том числе и Habrastorage;


Посмотреть как это работает можно на примере одной из моих статей;


Этот метод также можно использовать для редиректа ссылок на локальные статьи одна статья будет всегда ссылаться на другую;


Кликабельная титульная картинка


Часто заглавную картинку делают кликабельной, чтобы при клике открывалась сама статья. Для этого достаточно добавить такую строчку в исходник текста:


<linkmap src=HeaderImageLink dst=http://personeltest.ru/aways/habr.com/путь-к-статье />

}


Храним {



Исходники текста написаны, а публикуемые файлы получены с помощью конвертера. Теперь хотелось бы их сохранить в репозитории, но сначала необходимо определиться с структурой репозитория. Я разработал такую структуру:


  • Каждая статья хранится в определенной папке. Название этой папки перевод заголовка статьи, в котором пробелы заменены на дефисы, а запрещенные в url-символы игнорируются, например, для этой статьи названием будет являться следующее имя: Article-is-also-code;
  • Сам md файл внутри этой папки именуется языком, на котором эта статья написана, например, Russian.md или English.md;
  • Опционально. Локальные картинки и ресурсы хранятся либо в корневой папке статьи, либо в подпапке Images и маппятся с помощью утилиты MarkConv;

Использование такой структуры позволяет понять о чем статья и на каком языке она написана. Например, для этой статьи ссылка такая: https://gitlab.ptsecurity.com/writers/Articles/blob/master/Article-is-also-code/Russian.md;


В качестве навигации по документам, расположенным в репозитории, можно использовать файл README.md в корне репозитория, который содержит в себе список статей в хронологическом порядке, даты и порталы публикаций, описания и другую информацию;


Также имеет смысл хранить статью в приватном репозиторий до её публикации. А после публикации пушить ветку в зеркальный публичный репозиторий. В GitHub и GitLab такая возможность предоставляется бесплатно;


}


Вычитываем {



После того, как исходники статьи написаны и запушены, редакторы могут сделать "ревью" статьи, т.е. вычитку. Можно использовать знакомые для программиста инструменты: создавать issue, предлагать pull request и даже принимать участие в дискуссиях новой возможности GitHub;


После публикации статьи предлагать изменения сможет не только ограниченный круг лиц, но и все зарегистрированные пользователи. Так можно удобно исправлять имеющиеся опечатки;


}


Автоматизируем {



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


Готовый результат можно сразу копировать в черновик хабра, минуя запуск утилиты на локальном компьютере. Таким образом, становится возможным править статьи с помощью веб-браузера;


Я использую сервисы GitHub Actions и AppVeyor;


GitHub Actions


GitHub Actions удобно использовать когда вся разработка ведется на GitHub, к тому же он работает на приватных репозиториях даже в базовом бесплатном варианте;


В репозитории со статьями достаточно создать файл по аналогии с данным примером:


publish-articles.yml
name: Articles Converting and Publishingon: [push, pull_request]jobs:  build:    # Можно использовать и windows    runs-on: ubuntu-latest    steps:      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it      - uses: actions/checkout@v2      # Установка .NET Core      - name: Install .NET Core        uses: actions/setup-dotnet@v1        with:          dotnet-version: 3.1.101      # Установка .NET Core Tool      - name: Install MarkConv        run: dotnet tool install -g MarkConv.Cli --version 1.2.0 --add-source https://www.myget.org/F/mark-conv/api/v3/index.json      # Запуск утилиты MarkConv на все статьях в корневой папке      - name: Run MarkConv        run: markconv -f ./ -o Habr --checklinks      # Публикация сконвертированных статей      - name: Deploy Articles        uses: actions/upload-artifact@v2        with:          name: articles          path: ./_Output

AppVeyor


Данный сервис для меня даже оказался удобней, поскольку он может публиковать статьи по файлам, а не единым архивом. Конфигурация сервиса находится в файле appveyor.yml;


appveyor.yml
image:- Ubuntuversion: "{build}"skip_branch_with_pr: truebefore_build:- ps: |    dotnet tool install -g MarkConv.Cli --version 1.2.0 --add-source https://www.myget.org/F/mark-conv/api/v3/index.jsonbuild_script:- ps: |    markconv -f ./ -o Habr --checklinksafter_test:- ps: |    cd ./_Output    foreach ($file in Get-ChildItem ./)    {        Push-AppveyorArtifact $file.Name    }

}


Публикуем {



Артефакты получены, теперь можно копировать их на хабр и публиковать. К сожалению, этот шаг пока что невозможно автоматизировать, потому что "API Хабра закрыт на реконструкцию";


Надеюсь, в будущем публичный доступ восстановится, и процесс публикации статей станет более автоматизированным;


}


Заключение {


Итак, я пишу статьи в VS Code, храню их в Git, использую GitHub для разработки и автоматизирую с помощью GitHub Actions неплохой набор для программиста;


Я нашел для себя описанные наработки удобными для технических статей, но пока что слишком громоздким для одноразовых статей типа новостных. Мне нравится воспринимать статьи не просто как статический текст, который пишется один раз, а средство коммуникации, которое включает в себя меняющийся со временем контент и фидбек от других пользователей. Первого можно достичь, если представлять текст статьи в виде кода, второе предоставляет хабр и другие социальные сервисы;


Чтобы понять, удобны ли данные наработки для вас, я подготовил несколько опросов, после которых станет понятней, стоит ли дальше развивать эту концепцию;


В следующей статье я хотел бы рассказать о том, как разрабатывалась утилита для конвертации MarkConv, о ее технических деталях и возможном развитии;


}

Подробнее..
Категории: Usability , Git , Github , Github actions , Habr , Vscode , Typora , Pandoc , Appveyor

Из песочницы GitHub Actions и LaTeX поднимаем, заливаем

22.08.2020 14:05:15 | Автор: admin
В этой статье мы настроим пайплайн в GitHub для автоматической сборки pdf-файлов и последующей выкладки в Releases. Также поднимаем небольшой сайт-визитку с ссылкой на самые свежие сборки.

Материал будет полезен новичкам и тем, кто хочет быстро поднять CI/CD для latex встренными средствами GitHub.

Вступление


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

Основные требования были просты:

  1. Минимальными усилиями поднять сайт с релизами;
  2. Сделать обновления контента на сайте автоматическим.

В голове нарисовалось решение в виде пайплайна:

  1. Push коммита на GitHub;
  2. Сборка .tex-файлов в CI/CD;
  3. Отправка собранных pdf в GitHub releases;
  4. Обновление pdf-файлов на сайте-визитке.

В этой статье мы рассмотрим подробнее каждый шаг. В качестве сайта будет использоваться GitHub Pages. Для CI/CD будем использовать GitHub Actions.

Нам понадобится:

  • Аккаунт на GitHub;
  • Инструменты для компиляции LaTeX;
  • Любой текстовой редактор (я использую VIM, настроенный на latex);

Поехали!

Подключаем GitHub Actions


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

Заходим в Actions (подчеркнуто красным).


и находим там карточку Simple workflow, где нажимаем кнопку Set up this workflow


Перед нами откроется редактор с шаблоном workflow. На этом моменте стоит остановиться поподробнее.

GitHub Actions работает с Workflow, которые описываются в отдельных файлах. Каждый workflow состоит из:

  1. Имени (секция name: );
  2. Условия запуска (секция on: );
  3. Списка задач на выполнение (секция jobs: )

Каждая задача (job) тоже состоит из кусков поменьше, называемых step. Каждый step это атомарное действие (выполняемое полностью за раз). При этом step имеет свое имя (name: ) и список команд (run: ), а также может использовать уже готовый action (uses: ) от сторонних разработчиков.

Сторонние actions это самая мощная часть GitHub Actions. Они могут делать многие вещи: устанавливать JDK, запускать python-тесты в tox и многое другое. В данном мануале мы будем использовать xu-cheng/latex-action@v2 для компиляции latex (с ним не возникло проблем с кириллицей) и actions/upload-artifact@v2 для загрузки артефактов.

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

name: Build and deploy latex sources to GitHub Pageson: pushjobs:build:# Базовая ОС, на которой будут исполняться команды. Оставим ubunturuns-on: ubuntu-lateststeps:# Необходимо использовать этот action, чтобы получить содержимое репо- uses: actions/checkout@v2# Компилируем документ- name: Build documentuses: xu-cheng/latex-action@v2with:# Переименуйте, если у вас другой файлroot_file: main.tex# Больше параметров в офф. документацииworking_directory: latex_sources/# Аргументы, к которыми запускать компилятор (latexmk)# -jobname=<name> дает возможность поменять имя выходного файлаargs: -jobname=my_doc -pdf -file-line-error -halt-on-error -interaction=nonstopmodecompiler: latexmk# Загружаем собранные pdf-файлы- name: Upload pdf documentuses: actions/upload-artifact@v2with:# Это значение используется как ключ в хранилищеname: my_doc# Путь до собранного pdf. Может содержать *, **# Здесь это <working_directory>/<jobname>.pdfpath: latex_sources/my_doc.pdf

Сохраните это в файл, название файла выберете любым (можно latex.yml). После того как закоммитите создание файла в веб-редакторы, на GitHub Actions должен должна пойти первая сборка, по результатам которой появится артефакт собранный pdf.


Ура! Теперь можно приступать к релизам.

Настраиваем автоматические релизы


Система релизов в GitHub имеет одну особенность: релиз всегда привязан к коммиту с тэгом. Поэтому у нас есть два варианта:

  1. Ставить тэги вручную на те коммиты, по которым мы хотим собирать и релизить pdf-файлы;
  2. Ставить теги в автоматическом режиме на все коммиты и релизить их.

Для меня второй вариант показался более удобным, т.к. я обычно делаю коммит и пуш именно тогда, когда работа завершена (либо логически, либо на сегодняшний день). Поэтому в дальнейшем я буду рассказывать про него.

Для создания релиза мы будем использовать действие actions/create-release@v1 и для загрузки pdf-файла в созданный релиз (да, он загружается отдельно) используем actions/upload-release-asset@v1.

Добавим новый job:

deploy:runs-on: ubuntu-latest# Деплой будет только на ветке master. Закомментируйте, если не надоif: github.ref == 'refs/heads/master'# Можно зависеть от любого другого job. Порядок выполнения будет подстраиваться.needs: [build]steps:# Это хак, чтобы дергать bash-команды и запоминанать их результат- name: Variables# id используется внутренне: по нему можно ссылаться на результаты из другого stepid: vars# echo в таком форматировании позволит впоследствии ссылаться на результаты через ${{ steps.<step_id>.outputs.<variable_name> }}# Вертикальная черта |  это специальный символ yaml. Означает, что дальше идет массив команд и их все надо выполнитьrun: |echo ::set-output name=date::$(date +'%Y-%m-%d')echo ::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)- name: Download artifactsuses: actions/download-artifact@v2with:# Тот самый ключ, который мы указывали в upload-artifactname: my_doc- name: Create Releaseuses: actions/create-release@v1id: create_releaseenv:# По офф.документации, надо указать GITHUB_TOKENGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actionswith:# Берем результат из step с id=vars (см. выше).# Получим теги вида my_doc-<дата билда>-<первые 8 символов из sha коммита>tag_name: my_doc-${{ steps.vars.outputs.date }}-${{ steps.vars.outputs.sha8 }}# Имя, которое будет высвечиваться в релизеrelease_name: My Actions document (version ${{ steps.vars.outputs.date }})# Наш релиз не набросок и не пререлиз, так что оба в falsedraft: falseprerelease: false# Прикладываемые файлы надо заливать отдельным step- name: Upload pdf assetuses: actions/upload-release-asset@v1env:# Тоже требуется токенGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}with:# Из предыдущего step с id=create_release генерится upload_url  по нему и надо заливатьupload_url: ${{ steps.create_release.outputs.upload_url }}# Не переходим в папку latex_sources, поскольку download-artifacts грузит в текущую директориюasset_path: ./my_doc.pdf# Имя, которое будет высвечиваться в релизеasset_name: my_asset_name.pdfasset_content_type: application/pdf

Добавляем в файл workflow, коммитим изменения. Идем в Actions и видим, что добавилась еще один шаг:


При этом в releases тоже появлся собранный pdf.

Осталось дело за малым залить на сайт.

Поднимаем GitHub Pages


GitHub предоставляет возможность для каждого проекта создавать веб-страницу и дает бесплатный хостинг на нее. Но совершенно не обязательно владеть JS/CSS/HTML, чтобы написать что-то стоящее! Из коробки сервис предлагает несколько симпатичных шаблонов, которые полностью решают вопрос с версткой. От вас потребуется только заполнить Markdown-документ, а система сделает все остальное.

Идем в раздел Settings репозитория и во вкладке Options (открывается первой по-умолчанию) листаем вниз до GitHub Pages.


Тут в качестве source выбираем ветку master, а в качестве папки /docs (можно и корень /, но я предпочитаю держать минимальное количество файлов в корне проекта). Нажимаем Save.

Кнопкой Theme Chooser открывается галерея шаблонов, где каждый можно потыкать, посмотреть и выбрать нажатием на зеленую кнопку Select theme.

После выбора темы нас бросит в веб-редактор, где предлагается отредактировать Markdown-файл, который потом станет сайтом. Здесь можно описать все, что душе угодно: от простого представления себя до целей документа и особенностей работы.

Как только вы будете довольны содержимым, закоммитьте изменения.

Где моя страница?


Ссылка на собранную страницу всегда хранится в Settings -> GitHub Pages. Её лучше прописать в Website репозитория (шестеренка возле поля About на главной странице), чтобы не потерять.

Загружаем свежайший релиз


Есть небольшая хитрость: на последний релиз и все его файлы всегда можно сослаться, заменив тег коммита в URL на latest. В нашем примере, чтобы получить файл my_asset_name.pdf из последнего релиза, нужно вставить ссылку https://github.com/<your_username>/<repo_name>/releases/latest/download/my_asset_name.pdf.

В моем случае это было: https://github.com/alekseik1/github_actions_latex_template/releases/latest/download/my_asset_name.pdf.

После этих действий GitHub Pages всегда ссылаются на последний релиз.

Итоги


Мы настроили GitHub Actions на автоматическую сборку pdf-файла, выкладывание в релиз и подняли сайт на GitHub Pages, содержащий самую свежую версию. Финальную версию проекта можно найти здесь.

Спасибо за внимание!
Подробнее..
Категории: Ci/cd , Github , Github actions , Latex , Github pages

CICD для проекта в GitHub с развертыванием на AWSEC2

05.01.2021 00:04:18 | Автор: admin

Имеем: проект web API на.net core с исходниками в GitHub.

Хотим: авторазвертывание на виртуалке AWS EC2 после завершения работы с кодом (для примера push в develop ветку).

Инструментарий: GitHub Actions, AWS CodeDeploy, S3, EC2.

Ниже flow процесса, который будем реализовывать, погнали.

Basic CI/CDflowBasic CI/CDflow

1. Пользователи и роли

1.1. Пользователь для доступа к к AWS из GitHub Action

Этот пользователь будет использоваться для подключения к AWS сервисам S3 и CodeDeploy через AWS CLI 2 при запуске GitHub Actions.

1. Заходим в консоль AWS IAM, слева в меню User, и Add User

2. Задаем произвольное имя и ставим галочку Programmatic access

Создание нового пользователяСоздание нового пользователя

3. Далее, в разделе Permissions выбираем Attach existing policies directly и добавляем политики AmazonS3FullAccess и AWSCodeDeployDeployerAccess.

4. Тэги можно пропустить, на этапе Review должно получиться следующее:

Итоговый результатИтоговый результат

5. Жмем Create user и сохраняем данные пользователя у себя. Нам понадобятся Access Key ID и Secret Access Key позднее.

1.2. Сервисная роль для инстансов AWS EC2

Роль необходимо будет навесить на инстансы EC2, на которых будем производить развертывание, чтобы они могли взаимодействовать с сервисом AWS CodeDeploy.

  1. Заходим в консоль AWS IAM, слева в меню Role, и Add Role

  2. Выбираем AWS Service, в Choose a use case выбираем EC2 и переходим далее

  3. Находим и добавляем политику AmazonEC2RoleforAWSCodeDeploy.

  4. Тэги и Review пропускаем

  5. Называем роль, например, ProjectXCodeDeployInstanceRole и на этапе Review должно получиться следующее:

1.3. Сервисная роль для AWS CodeDeploy

Роль позволяет сервису AWS CodeDeploy обращаться к инстансам AWS EC2.

1. Заходим в консоль AWS IAM, слева в меню Role, и Add Role

2. Выбираем AWS Service, в Use case выбираем CodeDeploy:

Создание ролиСоздание роли

3. Переходим далее, нужная политика нам уже добавлена автоматом (AWSCodeDeployRole)

4. Называем роль, например, ProjectXCodeDeploy и на этапе Review должно получиться следующее:

Итоговый результат создания ролиИтоговый результат создания роли

2. Инстанс AWSEC2

  1. Разворачиваем подходящий инстанс в AWS EC2

  2. Назначаем ему роль ProjectXCodeDeployInstanceRole, созданную на этапе 1.2

  3. Устанавливаем CodeDeploy Agent на инстанс. Как это сделать смотрим по ссылке.

Важно: Если агента установили на машину до того, как навесили роль, то агента необходимо перезапустить командой sudo service codedeploy-agent restart

3. Конфигурация AWS CodeDeploy

1. Переходим в консоль AWS CodeDeploy

2. Слева в меню кликаем Deploy, внутри Applications

3. Создаем новое приложение кликнув Create application

4. Вводим произвольное имя, например, projectx и выбираем Compute platform EC2/On-Premises

Новое приложениеНовое приложение

5. Далее, провалившись в приложение кликаем Create deployment group

6. Назваем произвольно, develop в нашем случае. Роль выбираем ту, что создале в разделе 1.3 выше (ProjectXCodeDeploy).

7. Deployment type выбираем In place (для примера подойдет).

8. В разделе Environment configuration выбираем Amazon EC2 Instances и далее с помощью тэгов находим подходящие инстансы.

4. Создаем бакет на AWSS3

Для размещения новых сборок нам понадобится бакет в AWS S3.

  1. Заходим в консоль AWS S3, кликаем Create bucket.

  2. Называем, например, projectx-codedeploy-deployments. Убеждаемся что стоит галочка Block all public access. И после кликаем Create bucket.

Создание нового бакета S3Создание нового бакета S3

5. Создаем appspec.yml

Для того, чтобы CodeDeploy Agent понимал как работать с нашим приложением при получении новой сборки нужно ему об этом рассказать. В AWS CodeDeploy для этого нужен специальный файл в корне сборки под названием appspec.yml. В нашей задачке он выглядит следующим образом:

version: 0.0os: linuxfiles:  - source: /    destination: /opt/projectxpermissions:  - object: /opt/projectx    owner: ubuntu    group: ubuntu    type:      - directory      - filehooks:  ApplicationStart:    - location: scripts/start_server.sh      timeout: 300      runas: ubuntu  ApplicationStop:    - location: scripts/stop_server.sh      timeout: 300      runas: ubuntu
  1. Создаем appspec.yml version: 0.0 os: linux files:

В двух словах мы сообщаем, где исходники (строка 4), куда их разворачивать (строка 5) и с какими правами (строки 6-12). В разделе хуков мы говорим как поднимать и останавливать приложение. Подробнее по структуре и хукам на события можно почитать в документации.

Важно: Версию надо ставить 0.0, т.к. иначе ругается CodeDeploy и в веб-консоли будет ошибка что версия должна быть именно 0.0 \_()/. Второй момент: при первом деплойменте нужно закомментнтить хук ApplicationStop, иначе на нем будет падать развертывание. Связано это с тем, что агент пытается найти скрипты остановки в прошлом развертывании, которого еще не случилось. Второе и последующие развертывания работают с ApplicationStop корректно.

6. Настраиваем GitHubActions

Подобрались к самому интересному, настало время описать CI/CD pipeline на базе GitHub Actions.

6.1. Создаем секреты

На странице проекта в GitHub кликаем вкладку Settings, далее в меню слева Secrets и добавляем два секрета:

  • AWS_ACCESS_KEY_ID: значение мы сохранили на шаге 5 раздела 1.1 по созданию пользователь для доступа к AWS

  • AWS_SECRET_ACCESS_KEY: значение мы сохранили на шаге 5 раздела 1.1 по созданию пользователь для доступа к AWS

6.2. Настраиваем пайплайн

Создаем в корне приложения директорию .github/workflows. В ней будут размещаться наши файлы с описанием pipeline'ов. Создаем первый, например, develop.yaml. У нас получилось следующее:

name: build-app-actionon:   push:    branches:      - developjobs:  build:    name: CI part    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v2      - name: Setup .NET Core        uses: actions/setup-dotnet@v1        with:          dotnet-version: 5.0.101      - name: Install dependencies        run: dotnet restore      - name: Build        run: dotnet build --configuration Release --no-restore    deploy:    name: CD part    runs-on: ubuntu-latest    strategy:      matrix:        app-name: ['projectx']        s3-bucket: ['projectx-codedeploy-deployments']        s3-filename: ['develop-aws-codedeploy-${{ github.sha }}']        deploy-group: ['develop']    needs: build    steps:      - uses: actions/checkout@v2      # set up .net core      - name: Setup .NET Core        uses: actions/setup-dotnet@v1        with:          dotnet-version: 5.0.101      # restore packages and build      - name: Install dependencies        run: dotnet restore      - name: Build        run: dotnet build ProjectX --configuration Release --no-restore -o ./bin/app      # copying appspec file      - name: Copying appspec.yml        run: cp appspec.yml ./bin/app      # copying scripts      - name: Copying scripts        run: cp -R ./scripts ./bin/app/scripts            # Install AWS CLI 2      - name: Install AWS CLI 2        run: |          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"          unzip awscliv2.zip          sudo ./aws/install      # Configure AWS credentials      - name: Configure AWS Credentials        uses: aws-actions/configure-aws-credentials@v1        with:          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}          aws-region: ap-south-1      # Deploy push to S3      - name: AWS Deploy push        run: |          aws deploy push \          --application-name ${{ matrix.app-name }} \          --description "Revision of the ${{ matrix.appname }}-${{ github.sha }}" \          --ignore-hidden-files \          --s3-location s3://${{ matrix.s3-bucket }}/${{ matrix.s3-filename }}.zip \          --source ./bin/app      # Creating deployment via CodeDeploy      - name: Creating AWS Deployment        run: |          aws deploy create-deployment \          --application-name ${{ matrix.app-name }} \          --deployment-config-name CodeDeployDefault.AllAtOnce \          --deployment-group-name ${{ matrix.deploy-group }} \          --file-exists-behavior OVERWRITE \          --s3-location bucket=${{ matrix.s3-bucket }},key=${{ matrix.s3-filename }}.zip,bundleType=zip \

Глобально у нас два джоба: сборка и развертывание, причем первое является пререквизитом второго (раздел needs в deploy, строка 31). Срабатывание пайплайна в примере я настроил на push в ветку develop (строки 2-5).

Шаг build

Мы устанавливаем.net, скачиваем нужные пакеты и собираем проект. Именно это и указывается в строках 11-19. Сборку мы проводим на Ubuntu (строки 9 и 23) Здесь же можно прогнать unit-тесты. Если всё случилось, переходим к шагу deploy.

Шаг deploy

Создаем стратегию типа матрицы с параметрами для сборки.

  • app-name: название приложения как завели на шаге 4 раздела 3 по конфигурации CodeDeploy

  • s3-bucket: название бакета, который создали на шаге 2 раздела 4

  • s3-filename: шаблон имени файла для размещения в бакете, используем хэш коммита

  • deploy-group: название группы развертывания как завели на шаге 6 раздела 3.

Первые два шага аналогичны build: ставим.net, качаем пакеты, собираем приложение (тут точечно указан проект, т.к., например, тесты нам не нужны в артефакте, который собираемся катить) и складываем его в отдельную папку (./bin/app в нашем случае, строка 48). Копируем в папку с собранным приложением файл appspec.yml и скрипты запуска и остановки приложения (строки 43-48). Далее со строки 51 мы используем AWS CLI v2, который есть в виде готового action в маркетплейсе GitHub. Устанавливаем AWS CLI2, используем ключи от учетной записи из секретов GitHub проекта, которые настроили чуть выше в разделе 6.1, и указываем регион AWS. Загружаем в AWS S3 собранный проект. Создаем новое развертывание в AWS CodeDeploy. На этом заканчивается работа GitHub Actions.

После этого уже AWS CodeDeploy уведомит EC2 инстансы, которые были в нем настроены, о наличии новой сборки, CodeDeploy Agent на них сходит в AWS S3 за новой версией и развернет её. Понаблюдать за этим можно уже из консоли AWS CodeDeploy.

Резюмируем

К этому моменту у нас полностью завершена конфигурация и можно попробовать сделать push в GitHub и проверить как оно всё заработало.. или нет:) По ходу текста я постарался подсветить грабли, на которые наступил сам в ходе пробной настройки, поэтому надеюсь у вас все получилось сразу.

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

Подробнее..
Категории: Ci/cd , Devops , Github actions , Amazon web services , Aws

Публикация Go приложения в GitHub

15.02.2021 10:17:19 | Автор: admin

Пост представляет собой контрольный список (checklist) и его реализацию при публикации Go приложения на Github'е.

TLDR:

  • Makefile как входная точка для выполнения основных действий

  • Линтеры и тесты как инструменты повышающие качество кода

  • Dockerfile как способ распространения приложения

  • Github Actions как возможность автоматической сборки и выкладки приложения при новых изменениях

  • Goreleaser как инструмент для публикации релизов и пакетов

Результатом должно быть опубликованное приложение с настроенными инструментами которые позволяют легко сопровождать приложение в процессе его существования. В качестве реального примера я буду рассматривать утилиту pgcenter.


Disclaimer: Приведенный список не являются абсолютной истинной и является лишь субъективным списком вещей к которым я пришел в процессе публикации приложении. Список может дополняться и изменяться. Все это одно большое ИМХО, если у вас есть альтернативный взгляд или вы уверены/знаете что какие-то вещи можно сделать еще лучше, обязательно дайте знать в комментах.

Будем двигаться от простого к сложному. В случае встречи имен файлов, все они взяты относительно корня проекта где находится код приложения.

Makefile

Makefile хранит в себе служебные сценарии которые приходится часто выполнять при разработке приложения:

  • выполнение линтеров и тестов

  • сборка приложения

  • запуск приложения

  • сборка артефактов типа пакетов, docker образов и т.п.

  • публикация артефактов в сторонние репозитории

  • выполнение операций относительно внешних систем, например SQL миграции если речь идет о корпоративных приложениях В Makefile складываются рутинные операции выполнять которые приходится регулярно. Основная цель Makefile это помочь вам не держать в голове все нужные команды, параметры и аргументы, а собрать и их в одном месте и при необходимости выполнить их и получить результат. Позже Makefile также будет основным сценарием для запуска этих же рутинных процедур в CI/CD. Минимальная версия Makefile может выглядеть так:

PROGRAM_NAME = pgcenterCOMMIT=$(shell git rev-parse --short HEAD)BRANCH=$(shell git rev-parse --abbrev-ref HEAD)TAG=$(shell git describe --tags |cut -d- -f1)LDFLAGS = -ldflags "-X main.gitTag=${TAG} -X main.gitCommit=${COMMIT} -X main.gitBranch=${BRANCH}".PHONY: help clean dep build install uninstall .DEFAULT_GOAL := helphelp: ## Display this help screen.@echo "Makefile available targets:"@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "  * \033[36m%-15s\033[0m %s\n", $$1, $$2}'dep: ## Download the dependencies.go mod downloadbuild: dep ## Build pgcenter executable.mkdir -p ./binCGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${PROGRAM_NAME} ./cmdclean: ## Clean build directory.rm -f ./bin/${PROGRAM_NAME}rmdir ./bin

Давайте разберем важные моменты в приведенном Makefile.

  • В начале файла указываем служебные переменные которые потребуются дальше. Переменных немного, называние программы и информация из Git которая используется при сборке. В частности коммит, тег и ветка которые через сборщика пробрасываются в код приложения и используются для формирования версии программы. При таком подходе версия всегда берется из Git и не нужно хранить строку с версией в коде приложения, помнить о ней, своевременно обновлять. Отмечу что проброс переменных не автоматическая магия, предполагается что в Git используются теги и также есть соответствующие правки в коде для приема Git переменных. Например 1 и 2.

  • Следующий интересный момент это пункт help, он реализует справку для нашего Makefile - обратите внимание на комментарии начинающиеся с двойной решетки после названий пунктов. Эти комментарии как раз и используются в качестве справки если вызвать make без аргументов или явно make help.

  • Следующие пункты являются базовыми для жизненного цикла Go приложения: загрузка зависимостей, сборка и очистка каталога где хранится собранная программа.

При желании список пунктов можно дополнить, что я и буду делать дальше по ходу текста.

Линтеры и тесты

Следующий шаг добавление линтеров. Основная задача линтеров проверять код на предмет "странных" конструкций которые не соответствуют принятым стилям программирования или даже хуже, могут быть неоптимальными и приводить к каким-либо ошибкам. Наличие линтеров позволяют поддерживать хорошее качество кода, ориентироваться на правильный стиль написания кода принятый в языке. Использование линтеров необязательно, но крайне желательно. В go есть масса разных линтеров, я пришел пока к использованию golangci-lint и gosec. Первый включает в себя большой набор разных линтеров (включены при этом не все), второй является линтером с уклоном в соблюдение правил информационной безопасности.

Выполнение линтеров также регулярная задача, поэтому также добавляем их в Makefile

lint: dep ## Lint the source filesgolangci-lint run --timeout 5m -E golintgosec -quiet ./...

В приведенном случае для выполнения golangci-lint выставлен таймаут, через флаг -E включаются дополнительные линтеры. Выполнение gosec особо ничем не примечательно, просто рекурсивный обход каталогов.

Очевидно что golangci-lint и gosec должны быть установлены в системе. Их установка проста, описывать тут не буду.

Также код сопровождается тестами, добавим и их выполнение тоже.

test: dep ## Run testsgo test -race -p 1 -timeout 300s -coverprofile=.test_coverage.txt ./... && \    go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $$3}'@rm .test_coverage.txt

Команда запускает тесты, формирует файл с покрытием кода этими тестами и печатает результат покрытия.

Dockerfile

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

# stage 1: buildFROM golang:1.15 as buildLABEL stage=intermediateWORKDIR /appCOPY . .RUN make build# stage 2: scratchFROM scratch as scratchCOPY --from=build /app/bin/pgcenter /bin/pgcenterCMD ["pgcenter"]

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

Помимо сборки Docker образа потребуется место откуда другие пользователи смогут его забрать, например этим местом может быть Docker Hub. Для размещения там образа потребуется аккаунт и реквизиты (логин/пароль).

Сборка и публикация образа также регулярная операция, поэтому добавляем команды в Makefile.

docker-build: ## Build docker imagedocker build -t lesovsky/pgcenter:${TAG} .docker image prune --force --filter label=stage=intermediatedocker-push: ## Push docker image to registrydocker push lesovsky/pgcenter:${TAG}

Обратите внимание, что используется переменная TAG которая определяется в начале Makefile.

Github Actions

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

Однако в этом месте мы начинаем соприкасаться с организацией процесса, как новые изменения становятся частью существующего кода. Тема довольно обширная, об этом написано много постов, выработаны целые подходы со своими правилами. Поэтому те кто в теме уже и так все знают, а кто не в теме пусть отправляется на поиски чтива Git Flow, Github Flow, Gitlab Flow и их сравнение друг с другом.

В нашем случае все просто. Все изменения будут вливаться в master ветку. От ветки master мы создадим release ветку которая и будет источником релизов. Когда мы захотим сделать релиз, просто создадим новый тег и подтянем все изменения из master в release.

Теперь когда у нас есть понимание того как вносить изменения и делать релизы можно перейти к настройке workflow в Github Actions. Кто знает нормальный однословный русский перевод слову wokflow дайте знать в комментариях.

Будет два workflow:

  • Default (.github/workflows/default.yml) - это flow по-умолчанию, выполняет тесты при поступлении новых изменений.

  • Release (.github/workflows/release.yml) - это релизный flow делает тесты, сборку Docker образов и пакетов для пакетных менеджеров.

Этот workflow запускается при наступлении push и pull request событий в ветке master. Здесь всего одна задача (job), запуск линтеров и тестов. Github Actions имеет хорошие возможности для кастомизации и позволяют описывать очень сложные сценарии. В нашем случае таким примером кастомизации является запуск и выполнение в специально подготовленном контейнере где есть все необходимое для осуществления тестов.

Второй workflow.

---name: Releaseon:  push:    branches: [ release ]jobs:  test:    runs-on: ubuntu-latest    container: lesovsky/pgcenter-testing:v0.0.1    steps:      - name: Checkout code        uses: actions/checkout@v2      - name: Prepare test environment        run: prepare-test-environment.sh      - name: Run lint        run: make lint      - name: Run test        run: make test  build:    runs-on: ubuntu-latest    needs: test    steps:      - name: Checkout code        uses: actions/checkout@v2        with:          fetch-depth: 0      - name: Build image        run: make docker-build      - name: Log in to Docker Hub        run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}      - name: Push image to Docker Hub        run: make docker-push  goreleaser:    runs-on: ubuntu-latest    needs: [ test, build ]    steps:      - uses: actions/checkout@v2        with:          fetch-depth: 0      - uses: actions/setup-go@v2        with:          go-version: 1.15      - uses: goreleaser/goreleaser-action@v2        with:          version: latest          args: release --rm-dist        env:          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Релизный workflow чуть больше, и запускается он при push событиях в release ветке. Этот workflow также включает в себя выполнение линтеров и тестов. Но также тут есть еще две задачи - build и goreleaser.

В задаче build выполняется сборка и публикация Docker образа. Обратите внимание, что используются секреты, которые предварительно нужно указать в разделе Secrets, в настройках Github репозитория.

В задаче goreleaser выполняется публикация релиза в разделе Releases репозитория. Настройки goreleaser определим позже. Здесь также используется секрет GITHUB_TOKEN его указывать нигде не нужно, он создается автоматически для нужд workflow.

Goreleaser

Последний шаг это публикация релиза и дополнительная сборка пакетов. Кроме Docker образов существуют способы распространения с помощью пакетных менеджеров. Наиболее распространенные это deb и rpm пакеты. Есть и другие варианты, но для меня они экзотичны
и их я рассматривать не буду. Для всего этого нам потребуется goreleaser который и сделает всю работу по сборке. Настройки определяются в .goreleaser.yml

before:  hooks:  - make depbuilds:  - binary: pgcenter    main: ./cmd    goarch:      - amd64    goos:      - linux    env:      - CGO_ENABLED=0    ldflags:      - -a -installsuffix cgo      - -X main.gitTag={{.Tag}} -X main.gitCommit={{.Commit}} -X main.gitBranch={{.Branch}}archives:  - builds: [pgcenter]changelog:  sort: ascnfpms:  - vendor: pgcenter    homepage: https://github.com/lesovsky/pgcenter    maintainer: Alexey Lesovsky    description: Command-line admin tool for observing and troubleshooting Postgres.    license: BSD-3    formats: [ deb, rpm ]

Важным моментом является то что goreleaser не имеет интеграции с Makefile и сборку нужно описывать отдельно в формате goreleaser правил. Поэтому важно описать сборку точно так же как это осуществляется в Makefile, т.е. указать все те же флаги, переменные окружения и т.п. В секции nfpms описываем метаданные пакета и указываем необходимые форматы пакетов.

Собственно на этом все. Можно фиксировать изменения, пушить в репозиторий, перейти в Actions и наблюдать за тем как выполняются workflow. При успешном выполнении, можем создать новый тег, и влить изменения в релизную ветвь и также понаблюдать за прогрессом в Actions.

Спасибо за внимание, если у кого есть дополнения, замечания, вопросы, пожелания - Go в комменты.

Ссылки

Подробнее..
Категории: Github , Github actions , Go , Makefile , Goreleaser , Golangci-lint , Gosec

Бенчмарки VKUI и других ребят из UI-библиотек

26.05.2021 12:10:08 | Автор: admin

Меня зовут Григорий Горбовской, я работаю в Web-команде департамента по экосистемным продуктам ВКонтакте, занимаюсь разработкой VKUI.

Хочу вкратце рассказать, как мы написали 8 тестовых веб-приложений, подключили их к моно-репозиторию, автоматизировали аудит через Google Lighthouse с помощью GitHub Actions и как решали проблемы, с которыми столкнулись.

VKUI это полноценный UI-фреймворк, с помощью которого можно создавать интерфейсы, внешне неотличимые от тех, которые пользователь видит ВКонтакте. Он адаптивный, а это значит, что приложение на нём будет выглядеть хорошо как на смартфонах с iOS или Android, так и на больших экранах планшетах и даже десктопе. Сегодня VKUI используется практически во всех сервисах платформы VK Mini Apps и важных разделах приложения ВКонтакте, которые надо быстро обновлять независимо от магазинов.

VKUI также активно применяется для экранов универсального приложения VK для iPhone и iPad. Это крупное обновление с поддержкой планшетов на iPadOS мы представили 1 апреля.

Адаптивный экран на VKUIАдаптивный экран на VKUI

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

Какие задачи поставили

  1. Выявить главные проблемы производительности VKUI.

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

  3. Сравнить производительность VKUI и конкурирующих UI-фреймворков.

Технологический стек

Инструменты для организации процессов:

Чтобы проще взаимодействовать с несколькими веб-приложениями, в которых применяются разные UI-библиотеки, здесь используем lerna. Это удобный инструмент, с помощью которого мы объединили в большой проект ряд приложений с отличающимися зависимостями.

Бенчмарки мы проводим через Google Lighthouse официальный инструмент для измерения Web Vitals. Де-факто это стандарт индустрии для оценки производительности в вебе.

Самое важное делает GitHub Actions: связывает воедино сборку и аудит наших приложений.

Библиотеки, взятые для сравнения:

Название

Сайт или репозиторий

VKUI

github.com/VKCOM/VKUI

Material-UI

material-ui.com

Yandex UI

github.com/bem/yandex-ui

Fluent UI

github.com/microsoft/fluentui

Lightning

react.lightningdesignsystem.com

Adobe Spectrum

react-spectrum.adobe.com

Ant Design

ant.design

Framework7

framework7.io


Мы решили сравнить популярные UI-фреймворки, часть из которых основана на собственных дизайн-системах. В качестве базового шаблона на React использовали create-react-app, и на момент написания приложений брали самые актуальные версии библиотек.

Тестируемые приложения

Первым делом мы набросали 8 приложений. В каждом были такие страницы:

  1. Default страница с адаптивной вёрсткой, содержит по 23 подстраницы.

    • На главной находится лента с однотипным контентом и предложением ввести код подтверждения (абстрактный). При его вводе на несколько секунд отображается полноэкранный спиннер загрузки.

    • Страница настроек, в которую входит модальное окно с примитивным редактированием профиля и очередным предложением ввести код.

    • Страница с простым диалогом условно, с техподдержкой.

  2. List (Burn) страница со списком из 500 элементов. Главный аспект, который нам хотелось проверить: как вложенность кликабельных элементов влияет на показатель Performance.

  3. Modals страница с несколькими модальными окнами.

Не у всех UI-фреймворков есть аналогичные компоненты недостающие мы заменяли на равнозначные им по функциональности. Ближе всего к VKUI по компонентам и видам их отображения оказались Material-UI и Framework7.

Сделать 8 таких приложений поначалу кажется простой задачей, но спустя неделю просто упарываешься писать одно и то же, но с разными библиотеками. У каждого UI-фреймворка своя документация, API и особенности. С некоторыми я сталкивался впервые. Особенно запомнился Yandex UI кажется, совсем не предназначенный для использования сторонними разработчиками. Какие-то компоненты и описания параметров к ним удавалось найти, только копаясь в исходном коде. Ещё умилительно было обнаружить в компоненте хедера логотип Яндекса <3

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

Автоматизация

Краткая блок-схема, описывающая процессы в автоматизацииКраткая блок-схема, описывающая процессы в автоматизации

Подготовили два воркфлоу:

  • Build and Deploy здесь в первую очередь автоматизировали процессы сборки и разворачивания. Используем surge, чтобы быстро публиковать статичные приложения. Но постепенно перейдём к их запуску и аудиту внутри GitHub Actions воркеров.

  • Run Benchmarks а здесь создаётся issue-тикет в репозитории со ссылкой на активный воркфлоу, затем запускается Lighthouse CI Action по подготовленным ссылкам.

UI-фреймворк

URL на тестовое приложение

VKUI

vkui-benchmark.surge.sh

Ant Design

ant-benchmark.surge.sh

Material UI

mui-benchmark.surge.sh

Framework7

f7-benchmark.surge.sh

Fluent UI

fluent-benchmark.surge.sh

Lightning

lightning-benchmark.surge.sh

Yandex UI

yandex-benchmark.surge.sh

Adobe Spectrum

spectrum-benchmark.surge.sh


Конфигурация сейчас выглядит так:

{  "ci": {    "collect": {      "settings": {        "preset": "desktop", // Desktop-пресет        "maxWaitForFcp": 60000 // Время ожидания ответа от сервера      }    }  }}

После аудита всех приложений запускается задача finalize-report. Она форматирует полученные данные в удобную сравнительную таблицу (с помощью магии Markdown) и отправляет её комментарием к тикету. Удобненько, да?

Пример подобного репорта от 30 марта 2021 г.Пример подобного репорта от 30 марта 2021 г.

Нестабильность результатов

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

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

Предупреждения из отчётов Google LighthouseПредупреждения из отчётов Google Lighthouse

GitHub Actions по умолчанию выполняет задачи параллельно, как и в нашем воркфлоу с бенчмарками. Решение ограничить максимальное количество выполняющихся одновременно задач:

jobs.<job_id>.strategy.max-parallel: 1

Мы провели бенчмарк дважды, и вуаля результаты стали ощутимо стабильнее. Теперь можно переходить к результатам.

Результаты от 30 марта 2021 г.

VKUI (4.3.0) vs ant:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

ant

default

report

0.99

vkui (4.3.0)

modals

report

1

ant

modals

report

0.99

vkui (4.3.0)

list

report

0.94

ant

list

report

0.89

list - У ant нет схожего по сложности компонента для отрисовки сложных списков, но на 0,05 балла отстали.

VKUI (4.3.0) vs Framework7:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

f7

default

report

0.98

vkui (4.3.0)

modals

report

1

f7

modals

report

0.99

vkui (4.3.0)

list

report

0.94

f7

list

report

0.92

list - Framework7 не позволяет вложить одновременно checkbox и radio в компонент списка (List).

VKUI (4.3.0) vs Fluent:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

fluent

default

report

0.94

vkui (4.3.0)

modals

report

1

fluent

modals

report

0.99

vkui (4.3.0)

list

report

0.94

fluent

list

report

0.97

modals - Разница на уровне погрешности.

list - Fluent не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs Lightning:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

lightning

default

report

0.95

vkui (4.3.0)

modals

report

1

lightning

modals

report

1

vkui (4.3.0)

list

report

0.94

lightning

list

report

0.99

list - Lightning не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs mui:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

mui

default

report

0.93

vkui (4.3.0)

modals

report

1

mui

modals

report

0.96

vkui (4.3.0)

list

report

0.94

mui

list

report

0.77

default и modals - Расхождение незначительное, у Material-UI проседает First Contentful Paint.

list - При примерно одинаковой загруженности списков в Material-UI и VKUI выигрываем по Average Render Time почти в три раза (~1328,6 мс в Material-UI vs ~476,4 мс в VKUI).

VKUI (4.3.0) vs spectrum:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

spectrum

default

report

0.99

vkui (4.3.0)

modals

report

1

spectrum

modals

report

1

vkui (4.3.0)

list

report

0.94

spectrum

list

report

1

list - Spectrum не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs yandex:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

yandex

default

report

1

vkui (4.3.0)

modals

report

1

yandex

modals

report

1

vkui (4.3.0)

list

report

0.94

yandex

list

report

1

default - Разница на уровне погрешности.

list - Yandex-UI не имеет схожего по сложности компонента для отрисовки сложных списков.

modals - Модальные страницы в Yandex UI объективно легче.

Выводы из отчёта Lighthouse о VKUI

  • VKUI по показателям на одном уровне с другими библиотеками. Сильных просадок, как и значительного превосходства, нет.

  • Одно из явных проблемных мест вложенные Tappable протестированы на большом списке. Единственная библиотека, в которой полноценно реализован этот кейс, Material-UI. И VKUI уверенно обходит её по производительности.

  • Lighthouse ругается на стили после сборки много неиспользуемых. Они же замедляют First Contentful Paint. Над этим уже работают.

Два CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gzДва CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gz

Планы на будущее vkui-benchmarks

Переход с хостинга статики на локальное тестирование должен сократить погрешность: уменьшится вероятность того, что из-за внешнего фактора станет ниже балл у того или иного веб-приложения. Ещё у нас в репортах есть показатель CPU/Memory Power и он немного отличается в зависимости от воркеров, которые может дать GitHub. Из-за этого результаты в репортах могут разниться в пределах 0,010,03. Это можно решить введением перцентилей.

Также планируем сделать Lighthouse Server для сохранения статистики по бенчмаркам на каждый релиз.

Подробнее..

Словарь русского айти

05.07.2020 12:20:18 | Автор: admin

Всем привет! Меня зовут Азат, и сегодня мы поговорим о языке. Но не о языке программирования, а о естественном. Более конкретно, о языке русскоязычных айтишников. Как и у любого профессионального сообщества, у нас есть свой сленг (попробуйте дать навскидку несколько словечек). И сленг на самом деле немаленький.


И я люблю эти слова! Например "питоняша". Несмотря на то, что мне сложно дать определение этого слова (простая попытка "человек, очень любящий питон"), оно прекрасно, радует ухо.


Но если мы попробуем загуглить его, то получим всего 295 результатов. Возможно это, конечно, не самое популярное слово. Но если попробовать другие (например, "яндех"), то можно увидеть, что их присутствие в вебе не очень велико. Моё предположение в том, что русский айти сленг живёт в основном в бесконечных телеграм-чатах. Чат курса в университете, горы рабочих чатов, чаты по интересам к технологиям. Да вы и сами знаете.


В этих чатах слова рождаются и погибают. Мы фиксируем их в своих головах, но и оттуда они постепенно уходят. Мы теряем культуру.


А теперь представьте, если бы было единое место, где собран словарь русского айти. Это был бы своеобразный цифровой музей, в котором можно было бы познакомиться с бытом русскоязычного айти-специалиста. По-моему, очень классная идея. Я решил воплотить её в жизнь.


Реализация словаря


Думая, как же реализовать задуманное, я почти сразу вспомнил про другой проект сбор подписей в поддержку фигурантов "Московского дела" на гитхабе.


В этот репозиторий люди делают пулл реквест с файлом, в котором ставят своё имя и должность. После этого с помощью Github Actions их информация попадает в README и видна на главной странице. Мне нужно было практически то же самое.


После внимательного изучения кода (да что уж, многое просто скопировано) я сделал репозиторий с долгожданным словарём.


Алгоритм работы примерно такой: при пуше в мастер (а именно это происходит после одобрение пулл реквеста) запускается код на питоне, который сканирует целиком директорию с файлами, где находятся слова, делает небольшую валидацию. Потом берётся файл readme_header.md со статичным контентом. Его содержимое переносится в README и дописывается загруженными словами в алфавитном порядке. Потом скрипт на баше делает коммит и пуш, добавляя обновлённый файл в репозиторий.


Github Actions открывает много новых возможностей, реализация такого проекта на гитхабе без этого мне не представляется возможной. Если бы был голый README, в который люди добавляют новые слова, то постоянно ломался бы алфавитный порядок, к тому же добавились бы проблемы с мёрджем данных. В общем, неудобно. А с Github Actions всё круто. Можно легко сохранять структурированность, всё делается автоматически.


Всё должно работать. Теперь сообщество (то есть вы, друзья) может присылать новую информацию и она будет легко обрабатываться. Радость!


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


P. S. Если есть какие-то предложения по улучшению кода или вообще, то пишите мне в телеграм @Azatik1000

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru