Github actions
Github actions

Введение

Когда я создавал свой первый Python-пакет, dataclass-sqlalchemy-mixins(Github или pypi), я столкнулся с интересной задачей:как настроить CI/CD в GitHub так, чтобы при отправке новых изменений ничего не ломалось, а код автоматически публиковался в PyPI. О том, как опубликовать собственный пакет, я рассказывал в этой статье.

Обычно, чтобы проверить любой коммит, попадающий в master-ветку через pull request, необходимо запускать тесты. Кроме того, полезно использовать линтеры для проверки стиля кода, особенно если над проектом работают несколько разработчиков.

CI (Continuous Integration) — практика автоматической сборки и тестирования изменений в коде после их добавления в репозиторий.

CD (Continuous Delivery) — автоматизированная доставка кода в окружения разработки или продакшена.

Создание workflows

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

Workflows — это то, что GitHub будет запускать согласно вашей конфигурации.
Они должны находиться в директории .github/workflows и иметь формат .yaml.

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

name: "Test"

on:
  pull_request:
    types:
      - "opened"
      - "synchronize"
      - "reopened"
  push:
    branches:
      - '*'
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python 3.12
        id: setup-python
        uses: actions/setup-python@v5
        with:
          python-version: 3.12

      - name: Cache poetry install
        uses: actions/cache@v4
        with:
          path: ~/.local
          key: poetry-${{ steps.setup-python.outputs.python-version }}-1.7.1-0

      - uses: snok/install-poetry@v1
        with:
          version: 1.7.1
          virtualenvs-create: true
          virtualenvs-in-project: true

      - name: Load cached venv
        id: cached-poetry-dependencies
        uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

      - name: Install dependencies
        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
        run: poetry install --no-interaction

      - name: Run tests
        run: poetry run pytest  --cov-report=xml

Разберём основные элементы этого файла:

  • name — имя workflow.

  • pull_request — указывает, какие типы pull request будут запускать workflow.По умолчанию обычно рекомендуются openedsynchronize и reopened.Подробнее об этих типах можно прочитать здесь.

  • push — указывает ветки, при push в которые будет запускаться workflow.

  • jobs — workflow может состоять из одной или нескольких job, которые по умолчанию выполняются параллельно, но при необходимости их можно настроить и на последовательный запуск.

Теперь чуть подробнее о том, что происходит при выполнении job в этом примере. Во-первых, имя job можно использовать для настройки rulesets в репозитории или для конкретных веток. В данном случае будет запускаться только одна job — test. Про rulesets подробнее поговорим позже.

Ключ runs-on указывает, на какой операционной системе будет выполняться job. Настройка strategy: fail-fast: false означает, что GitHub не будет отменять все выполняющиеся и ожидающие job в matrix, если одна из job в матрице завершится с ошибкой. Параметр matrix мы рассмотрим немного позже.

Как видно, в steps много похожих записей в ключе uses. Например:

  • actions/setup-python@v5

  • actions/cache@v4

  • snok/install-poetry@v1

а первый взгляд названия actions могут сбивать с толку, но на самом деле это просто пути к GitHub-репозиториям этих actions, без части @v. Например, исходный код первой action можно посмотреть по адресу https://github.com/actions/setup-python . To же самое относится и к другим actions. Часть @v используется для указания версии action.

Ключ with нужен для передачи параметров в action.Например, если мы хотим установить Python 3.12, мы передаём python-version: 3.12 в шаге Set up Python 3.12. Тот же подход применяется и в других шагах.

Если коротко, job test выполняет следующие действия:

  1. Устанавливает Python нужной версии.

  2. Устанавливает зависимость Poetry 1.7.1 с использованием кэша GitHub.

  3. Создаёт Python .venv в корне проекта, если кэш не найден. Кэш виртуального окружения зависит от версии Python и poetry.lock.

  4. Если закэшированное виртуальное окружение не найдено, устанавливаются зависимости проекта.

  5. Запускаются тесты.

Использование сервисов в workflows

Созданный workflow довольно простой и может не подойти для тестирования более сложных приложений или проектов. Дело в том, что таким проектам часто нужны дополнительные сервисы — например, базы данных или кэш. Конечно, можно писать тесты, которые будут мокать запросы к этим сервисам, но я бы не рекомендовал такой подход, так как он может снизить эффективность тестового покрытия. Я покажу, как добавить сервис в workflow, на примере PostgreSQL.

jobs:
  test:
    services:
      postgres:
        image: postgres:latest
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

Добавление ключа services с именем нужного сервиса позволяет поднять его во время выполнения job.Более того, ему можно передавать настройки почти так же, как при использовании docker.

Использование нескольких параметров при запуске job

И наконец, важный случай — когда job нужно запускать с разными параметрами. Например, вам может понадобиться запускать её не только на последней версии Python, но и на нескольких версиях сразу, либо с разными зависимостями. Это особенно полезно, если нужно поддерживать обратную совместимость со старыми версиями зависимостей. Для этого в GitHub есть ключ strategy. Параметр matrix позволяет запускать одну и ту же job с несколькими наборами значений.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12"]
        sqlalchemy-version: ["1.4.52", "2.0.31"]

Job с такой матрицей будет запускаться для каждой комбинации значений параметров.
В результате мы получим четыре запуска job со следующими сочетаниями версий Python и SQLAlchemy:

  • 3.111.4.52

  • 3.112.0.31

  • 3.121.4.52

  • 3.122.0.31

Доступ к параметрам внутри job можно получить через ${{ matrix.python-version }} или ${{ matrix.sqlalchemy-version }}.

      - name: Set up Python ${{ matrix.python-version }}
        id: setup-python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install sqlalchemy ${{ matrix.sqlalchemy-version }}
        run: pip install sqlalchemy==${{ matrix.sqlalchemy-version }}

Использование GitHub rulesets

Но иногда простого запуска job для проекта недостаточно. Бывают ситуации, когда нужно ограничить возможность слияния коммитов в master-ветку. Если вы работаете над проектом один, это может быть не так критично, но при командной разработке такая настройка крайне желательна. Она помогает избежать ситуации, когда кто-то случайно сливает коммит, который ломает код, даже если пайплайны падают.

Для этого GitHub предоставляет удобный инструмент под названием rulesets.Открыть его можно в настройках репозитория по адресу https://github.com/{Author}/{Repository}/settings/rules .После создания нового ruleset прокрутите страницу вниз до пункта Require status checks to pass, включите его и укажите, какие job должны завершаться успешно.Чтобы найти нужную проверку, начните вводить имя требуемой job, и GitHub предложит варианты.Обратите внимание: каждая job в matrix будет иметь собственное имя, поэтому их можно выбирать по отдельности.

Публикация в PyPI

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

name: Upload Python Package to PyPi on release

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  deploy:

    runs-on: ubuntu-latest

    environment:
      name: pypi
      url: https://pypi.org/project/dataclass-sqlalchemy-mixins/

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v3
      with:
        python-version: '3.x'

    - uses: snok/install-poetry@v1
      with:
        version: 1.7.1
        virtualenvs-create: true
        virtualenvs-in-project: true

    - name: Build package
      run: poetry build

    - name: Publish package
      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
      with:
        user: __token__
        password: ${{ secrets.PYPI_API_TOKEN }}

Этот workflow:

  1. Устанавливает Python.

  2. Устанавливает зависимость Poetry 1.7.1 с использованием кэша GitHub.

  3. Собирает source- и wheel-архивы.

  4. Публикует пакет в PyPI.

Да, всё действительно настолько просто, как выглядит. Вам нужно только добавить PYPI_API_TOKEN в secrets репозитория (https://github.com/{Author}/{Repository}/settings/secrets/actions ). После этого создайте новый release (https://github.com/{Author}/{Repository}/releases), и пакет будет автоматически загружен.

Комментарии (0)