Предпосылки

Это продолжение моей предыдущей статьи: Монорепо с использованием Lerna, обычных коммитов и пакетов Github. Предварительные условия предыдущей статьи предполагают понимание этой, поэтому вы можете сначала прочитать ее для дополнительного контекста.

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

Контекст

Непрерывная интеграция (CI)

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

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

Действия на гитхабе

Действия Github — это функция Github, которая позволяет разработчикам выполнять рабочие процессы, когда в наших репозиториях происходят определенные события, такие как отправка кода или закрытие запроса на извлечение (часто используется в сценариях непрерывной интеграции). Действия Github бесплатны для общедоступных репозиториев.

Эти рабочие процессы организованы в jobs, steps и actions во вложенном виде и запускаются одним или несколькими events. Каждый рабочий процесс представляет собой отдельный файл, написанный на языке YAML.

Что будем строить?

Мы собираемся автоматизировать управление версиями и публикацию пакетов в нашем монорепозитории с помощью Lernaобычными коммитами) и действий Github.

Мы собираемся реализовать два разных рабочих процесса Github:

1 — Checks рабочий процесс: когда мы открываем новый запрос на вытягивание или вносим изменения в открытый запрос на вытягивание, он запускает ряд проверок, которые мы считаем необходимыми для интеграции изменений в нашу кодовую базу.

2 — Publish рабочий процесс: всякий раз, когда запрос на слияние объединяется, мы запускаем рабочий процесс, который будет версионировать и публиковать наши пакеты. Он будет вести себя немного по-разному в зависимости от ветки назначения:

  • При слиянии с веткой development будут публиковаться бета-версии измененных пакетов (подходят для контроля качества или тестирования).
  • При слиянии с веткой main будут опубликованы окончательные версии (готовые к производству).

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

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

Руки вверх

Часть 1 — Проверка рабочего процесса при открытии/изменении PR

Github ожидает, что рабочие процессы будут расположены в ${projectFolder}/.github/workflows, поэтому давайте создадим новую ветку Github и добавим наш первый рабочий процесс checks.yaml в этот каталог (вы также можете создавать рабочие процессы из пользовательского интерфейса Github):

Структура проекта выглядит так:

/
  .github/
    workflows/
      checks.yaml
  [...]

Теперь давайте начнем работать над рабочим процессом. Откройте файл checks.yaml в редакторе и добавьте следующие атрибуты:

name: Checks # Workflow name

on:
  pull_request:
    types: [opened, synchronize] # Workflow triggering events
  • name: имя рабочего процесса.
  • on: прослушиватель событий, запускающих этот рабочий процесс. В нашем случае он будет срабатывать каждый раз, когда запрос на включение открывается или модифицируется.

Далее мы добавим задание в рабочий процесс и настроим тип экземпляра, который Github будет запускать для его запуска с атрибутом runs-on:

name: Checks
on:
  pull_request:
    types: [opened, synchronize]

jobs: # A workflow can have multiple jobs
  checks: # Name of the job
    runs-on: ubuntu-latest # Instance type where job will run

Эта работа будет состоять из нескольких шагов:

  • Checkout: Получите код из репозитория, в котором определен рабочий процесс.
  • Setup NodeJS: Настройте NodeJS с определенной версией.
  • Setup npm: Поскольку мы будем устанавливать зависимости из нашего частного реестра (в пакетах Github), мы должны добавить их в конфигурацию npm.
  • Install dependencies: Установите необходимые пакеты npm.
  • Run tests: Выполните тесты, если они есть.

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

Пользовательские и общедоступные действия

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

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

Давайте реализуем первые два шага задания build:

name: Checks
on:
  pull_request:
    types: [opened, synchronize]
jobs:
  check:
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout" # Download code from the repository
      uses: actions/checkout@v2 # Public action
      with:
        fetch-depth: 0 # Checkout all branches and tags

    - name: "Use NodeJS 14" # Setup node using version 14
      uses: actions/setup-node@v2 # Public action
      with: 
        node-version: '14'
  • Шаг Checkout загрузит код из репозитория. Мы должны добавить опцию depth: 0, чтобы Lerna могла правильно отслеживать теги опубликованных версий пакетов и предлагать новые версии при обнаружении изменений.
  • На шаге Use NodeJS 14 мы настраиваем NodeJS для использования версии 14, но мы могли бы даже выполнить его для нескольких версий одновременно, используя матрицу.

Давайте зафиксируем и отправим эту версию рабочего процесса на Github, а затем откроем запрос на извлечение (если у вас еще нет созданной ветки development, создайте ее из main, потому что мы откроем для нее запрос на извлечение).

Как только запрос на слияние будет открыт, наш рабочий процесс будет выполнен. Откройте браузер и перейдите в раздел «Действия» репозитория, чтобы увидеть результат выполнения:

Если мы нажмем на него, мы сможем увидеть детали выполнения, а нажав на любое из заданий (в нашем случае на задание checks), мы сможем увидеть статус и результаты каждого из его шагов:

Давайте добавим следующий шаг: Setup npm. На этом шаге мы добавим наш реестр пакетов Github в файл .npmrc, чтобы npm мог найти пакеты, опубликованные в нашем реестре пакетов Github.

В каждом пошаговом действии может выполняться одна или несколько команд. В этом случае мы запустим пару команд npm set в одном действии:

name: Checks
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout"
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: "Use NodeJS 14"
      uses: actions/setup-node@v2
      with: 
        node-version: '14'

    - name: "Setup npm" # Add our registry to npm config
      run: | # Custom action
        npm set @xcanchal:registry=https://npm.pkg.github.com/xcanchal
        npm set "//npm.pkg.github.com/:_authToken=${{ secrets.PUBLISH_PACKAGES }}"

Секреты рабочего процесса и переменные среды

В предыдущем фрагменте вы заметили ${{ secrets.PUBLISH_PACKAGES }}. Не рекомендуется загружать секреты в наши репозитории, и на Github есть решение для этого. Мы добавим новый секрет репозитория, который сможет использовать его позже в наших рабочих процессах.

В браузере перейдите в Репозиторий › Настройки › Секреты и нажмите Новый секрет репозитория. Добавьте описательное имя и значение, которое должно быть токеном личного доступа Github с разрешениями на чтение и запись пакетов в ваш реестр пакетов Github (см. мою предыдущую статью для получения инструкций о том, как его создать).

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

Далее мы добавим еще один шаг: Install dependencies. На этом шаге мы установим корневые зависимости в производственном режиме (см. команду npm ci), а также запустим lerna bootstrap для установки зависимостей для каждого из наших пакетов и создадим связи между ними.

name: Checks
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout"
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: "Use NodeJS 14"
      uses: actions/setup-node@v2
      with:
        node-version: '14'

    - name: "Setup npm"
      run: |
        npm set @xcanchal:registry=https://npm.pkg.github.com/xcanchal
        npm set "//npm.pkg.github.com/:_authToken=${{ secrets.PUBLISH_PACKAGES }}"

    - name: Install dependencies
      run: | # Install and link dependencies
        npm ci
        npx lerna bootstrap

Зафиксируйте и отправьте изменения и посмотрите, как событие «Pull Request synchronized» запускает наш рабочий процесс, который теперь содержит последние добавленные шаги:

Перед добавлением нашего последнего шага Running tests нам нужно внести изменения в наши пакеты date-logic и date-renderer, изменив тестовый скрипт npm. Поскольку мы еще не реализовали ни одного фактического теста, мы просто выведем «ТЕСТЫ ПРОЙДЕНЫ» при выполнении этой команды.

Измените тестовый сценарий в package.json пакета date-logic.

# package.json
"scripts": {
  "test": "echo TESTS PASSED"
}

и внесите изменения в репо. Затем повторите тот же процесс для date-renderer.

# commit and push
$ git add .
$ git commit -m "feat(date-logic): echo tests"
$ git push

После отправки новой тестовой команды в наши пакеты мы можем добавить шаг Running tests в наш рабочий процесс.

name: Checks
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout"
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: "Use NodeJS 14"
      uses: actions/setup-node@v2
      with:
        node-version: '14'

    - name: "Setup npm"
      run: |
        npm set @xcanchal:registry=https://npm.pkg.github.com/xcanchal
        npm set "//npm.pkg.github.com/:_authToken=${{ secrets.PUBLISH_PACKAGES }}"

    - name: Install dependencies
      run: |
        npm ci
        npx lerna bootstrap

    - name: Run tests # Run tests of all packages
      run: npx lerna exec npm run test

Отправьте изменения в репозиторий и посмотрите результаты выполнения в разделе действий Github:

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

Часть 2. Рабочий процесс публикации в объединенном PR

Создайте файл publish.yaml в репозитории workflows со следующим содержимым. Вы заметите, что мы добавили новый атрибут branches в прослушиватели событий. С этой конфигурацией мы сообщаем Github, что этот рабочий процесс выполняется только тогда, когда запрос на слияние объединяется с веткой development или main.

name: Publish

on:
  pull_request:
    types: [closed]
    branches:
      - development
      - main

Теперь мы добавим в этот рабочий процесс задание с именем publish, атрибут runs-on и новый, который мы еще не использовали: if. Этот атрибут используется для оценки выражения для условного запуска задания, если оно оценивается как истинное или ложное (его также можно использовать в шагах).

В соответствии с атрибутом on, который мы настроили, этот рабочий процесс будет запускаться при каждом событии «Закрытие запроса на извлечение» для development или main, но на самом деле мы хотим, чтобы он выполнялся ТОЛЬКО тогда, когда запрос на извлечение был объединен (не отброшен). Поэтому мы должны добавить к заданию условие github.event.pull_request.merged == true:

name: Publish
on:
  pull_request:
    types: [closed]
    branches:
      - development
      - main

jobs:
  publish:
    if: github.event.pull_request.merged == true # Condition
    runs-on: ubuntu-latest

Теперь давайте воспроизведем те же самые первые три шага, которые мы добавили в рабочий процесс проверок (Checkout, Use NodeJS 14 и Setup npm).

name: Publish

on:
  pull_request:
    types: [closed]
    branches:
      - development
      - main

jobs:
  publish:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout"
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: "Use NodeJS 14"
      uses: actions/setup-node@v2
      with:
        node-version: '14'

    - name: "Setup npm"
      run: |
        npm set @xcanchal:registry=https://npm.pkg.github.com/xcanchal
        npm set "//npm.pkg.github.com/:_authToken=${{ secrets.PUBLISH_PACKAGES }}"

Наконец, мы добавим последний (и интересный) шаг: Publish and version. Разберем подробно атрибуты шага и команды внутри действия:

  • Поскольку Лерна будет отвечать за публикацию новых версий пакетов, мы должны установить переменную среды GH_TOKEN с нашим токеном личного доступа в качестве значения, чтобы у Лерны были необходимые разрешения.
  • Нам нужно добавить пару строк конфигурации Github, чтобы указать имя пользователя и учетные данные электронной почты, чтобы Лерна могла делать коммиты и создавать теги для новых версий в репозитории. Для этого воспользуемся переменной github.actor, доступной в среде.
  • В операторе if/else мы проверяем переменную ${{ github.base_ref }}, чтобы узнать, является ли целевая ветвь PR development. В этом случае мы отправим флаги --conventional-prerelease и --preid команде версии Lerna для создания бета-версий. В противном случае (это может быть только main, потому что мы ограничили на уровне рабочего процесса, что это должна быть одна из этих двух ветвей), мы будем использовать аргумент --conventional-graduate для создания окончательных версий. И последнее, но не менее важное: флаг --yes автоматически подтверждает версию и операции публикации (в противном случае Lerna запросит ручное подтверждение, и CI завершится ошибкой).
name: Publish
on:
  pull_request:
    types: [closed]
    branches:
      - development
      - main

jobs:
  publish:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest

    steps:
    - name: "Checkout"
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: "Use NodeJS 14"
      uses: actions/setup-node@v2
      with:
        node-version: '14'

    - name: "Version and publish" # Interesting step
      env:
        GH_TOKEN: ${{ secrets.PUBLISH_PACKAGES }}
      run: |
        git config user.name "${{ github.actor }}"
        git config user.email "${{ github.actor}}@users.noreply.github.com"

        if [ ${{ github.base_ref }} = development ]; then
          npx lerna version --conventional-commits --conventional-prerelease --preid beta --yes
        else
          npx lerna version --conventional-commits --conventional-graduate --yes
        fi

        npx lerna publish from-git --yes

Давайте зафиксируем новый рабочий процесс в репозитории, а затем объединим запрос на слияние, чтобы он сработал. Если мы проверим вывод шага Version and publish, мы увидим много информации о двух шагах, которые выполнил Лерна:

1) При запуске команды lerna version были обнаружены изменения в пакетах и ​​предложены новые бета-версии (обратите внимание на префикс -beta.1), которые были приняты автоматически. После этого он отправил теги версии в репозиторий Github:

2) При выполнении команды lerna publish from-git она проанализировала последние теги Github, чтобы определить версии, которые необходимо опубликовать, и опубликовала пакеты в реестре пакетов Github.

Итак, теперь у нас есть несколько тестовых версий в нашем реестре пакетов Github:

Предположим, что они прошли тестирование и помечены как готовые к производству. Давайте создадим новый Pull Request от development против master, объединим его и посмотрим, как выполняется тот же самый Publish джоб, но на этот раз Lerna опубликует окончательные версии:

Заключение

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

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

Следующие шаги

  • Настройте правила защиты веток Github, чтобы заблокировать кнопку слияния запросов на вытягивание в случае сбоя рабочего процесса checks.
  • Настройте средство проверки синтаксиса фиксации (например, commitlint), чтобы избежать человеческих ошибок, которые могут повлиять на управление версиями из-за противоречивой истории фиксации.

Первоначально опубликовано на https://dev.to 4 октября 2021 г.