$ sudo teach IT
Модуль 6 · Интеграция

CI/CD — GitHub Actions

Автоматическая проверка кода в каждом pull request и каждом коммите.

🔌 Интеграция 🕐 ~35 минут
🎯

Зачем Ruff в CI/CD

Локальный запуск Ruff — это отлично, но настоящая ценность линтера раскрывается, когда он запускается автоматически в CI/CD. Без автоматизации проверки кода человеческий фактор неизбежен: кто-то забудет запустить Ruff перед коммитом, кто-то проигнорирует предупреждения, а кто-то вообще не будет использовать линтер. CI/CD решает эту проблему: каждый коммит и каждый pull request проходят автоматическую проверку, и код с ошибками просто не может попасть в основную ветку.

Преимущества автоматической проверки кода в CI/CD:

  • Гарантия качества — ни один коммит с синтаксическими ошибками или нарушением стиля не попадёт в main.
  • Скорость ревью — код-ревьюеры тратят меньше времени на стилистические замечания и больше на логику.
  • Единый стандарт — вся команда автоматически следует одним правилам, независимо от личных предпочтений.
  • Экономия времени — Ruff делает проверку за секунды, а не минуты, как другие инструменты.
  • Автоматические аннотации — ошибки отображаются прямо в diff Pull Request на GitHub.
⚠️ Даже если в вашей команде все дисциплинированно запускают Ruff локально — всегда добавляйте его в CI/CD. Это «последняя линия обороны», которая ловит человеческие ошибки.
📋

Основные подходы к интеграции

Есть три основных способа запустить Ruff в GitHub Actions, и выбор между ними зависит от ваших потребностей:

Подход Способ установки Скорость Когда использовать
astral-sh/ruff-action Официальный Action Высокая Рекомендуемый способ
pip install ruff pip в виртуальном окружении Средняя Уже есть Python setup
uvx ruff Через astral-sh/setup-uv Очень высокая Проект уже использует uv
⚙️

Базовый workflow с ruff-action

astral-sh/ruff-action — официальный GitHub Action от команды Ruff. Он автоматически загружает последнюю версию Ruff, устанавливает её и запускает проверку. Это самый простой и рекомендуемый способ интеграции.

Создай файл .github/workflows/lint.yml в корне репозитория:

.github/workflows/lint.yml
name: Lint

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

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3

Этот минимальный workflow:

  • Запускается на push в main/master и на каждый pull request
  • Выполняет ruff check на всём проекте
  • Если найдены ошибки — workflow завершается с кодом 1 (failure)
💡 astral-sh/ruff-action@v3 — официальный Action от команды Ruff. Устанавливает Ruff и запускает проверку автоматически. Версия v3 — актуальная стабильная версия Action.

Вот что делает Action «под капотом»:

  1. Проверяет, установлен ли уже Ruff в кеше GitHub Actions
  2. Если нет — скачивает готовый бинарник с GitHub Releases
  3. Сохраняет бинарник в кеш для последующих запусков
  4. Запускает ruff check с переданными аргументами
🔧

Параметры ruff-action

Action поддерживает ряд параметров, которые позволяют тонко настроить его поведение:

Параметр Описание По умолчанию
version Версия Ruff для установки (например "0.9.0") latest
args Дополнительные аргументы командной строки —
src Путь к файлам для проверки

Пример с указанием версии и дополнительных аргументов:

- uses: astral-sh/ruff-action@v3
  with:
    version: "0.9.0"
    args: --output-format=github --line-length=120 --target-version=py311
📌

Аннотации в Pull Request

Одна из самых полезных возможностей Ruff в GitHub Actions — автоматические аннотации ошибок в Pull Request. Когда Ruff запускается с флагом --output-format=github, он выводит результаты в формате, который GitHub Actions автоматически преобразует в аннотации прямо в diff затронутых файлов.

📌 Без --output-format=github ошибки Ruff будут видны только в логах workflow. С этим флагом они появляются прямо в diff PR — напротив тех строк, где найдены ошибки, с указанием правила и описания.

Пример workflow с аннотациями через astral-sh/ruff-action:

name: Lint with Annotations

on: [push, pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: --output-format=github

Если вы предпочитаете устанавливать Ruff через pip (например, если вы хотите контролировать версию через зависимости проекта):

name: Lint

on: [push, pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Ruff
        run: pip install ruff

      - name: Lint
        run: ruff check --output-format=github .

      - name: Format check
        run: ruff format --check .

Разберём этот workflow по шагам:

  1. actions/checkout@v4 — клонирует репозиторий на runner
  2. actions/setup-python@v5 — устанавливает Python 3.12
  3. pip install ruff — устанавливает Ruff через pip
  4. ruff check --output-format=github . — запускает линтинг с аннотациями
  5. ruff format --check . — проверяет форматирование (без изменений, только проверка)
⚠️ ruff format --check . завершается с ошибкой (exit code 1), если файлы не отформатированы. Используйте ruff format . (без --check) только если вы хотите, чтобы CI автоматически форматировал код и коммитил изменения.
📄

Форматы вывода для CI

Ruff поддерживает несколько форматов вывода, каждый из которых имеет свои преимущества в CI/CD:

Формат Команда Когда использовать
GitHub --output-format=github Аннотации в PR на GitHub
JSON --output-format=json Программная обработка результатов
Concise По умолчанию Читаемый вывод в логах
Full --output-format=full Максимально подробный вывод
JUnit --output-format=junit Интеграция с CI-панелями

GitHub-формат особенно полезен: он выводит ошибки в формате ::warning file={file},line={line},col={col},title={code}::{message}, который GitHub Actions автоматически парсит и показывает как аннотации прямо в diff Pull Request. Ревьюеру не нужно открывать логи workflow — ошибки видны прямо в интерфейсе просмотра кода.

🚀

Быстрый вариант через uv

Если ваш проект уже использует uv (быстрый менеджер пакетов от Astral), вы можете запускать Ruff через uvx — утилиту, которая запускает инструменты без их явной установки. uvx скачивает и кеширует бинарник Ruff при первом запуске, а при повторных использует локальный кеш.

Преимущества uv-подхода в CI:

  • Минимальное время установки — uv написан на Rust и работает намного быстрее pip
  • Автоматическое кеширование — uv кеширует бинарники между запусками
  • Не нужен Python — uvx может запускать Ruff без установленного Python
  • Единый инструментарий — если вы уже используете uv для управления проектом
name: Lint

on: [push, pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4

      - name: Lint
        run: uvx ruff check --output-format=github .

      - name: Format check
        run: uvx ruff format --check .

Что происходит на каждом шаге:

  1. astral-sh/setup-uv@v4 — устанавливает uv (занимает ~1 секунду)
  2. uvx ruff check — uvx проверяет, есть ли ruff в кеше. Если нет — скачивает и кеширует. Затем запускает.
  3. uvx ruff format --check — то же для проверки форматирования. Бинарник уже закеширован.

Полный рабочий процесс занимает 5–10 секунд на типичном проекте, что значительно быстрее альтернатив с pip.

💡 uvx — это аналог npx из мира Node.js. Он позволяет запускать любой Python-инструмент одной командой без предварительной установки. Если инструмент уже есть в кеше uv — запуск происходит мгновенно.
🧩

Матричный запуск (matrix)

GitHub Actions поддерживает матричные сборки — запуск одного и того же workflow с разными параметрами. Это полезно, если вы хотите проверить код на нескольких версиях Python, на разных ОС или с разными настройками Ruff.

Пример матричного запуска Ruff на нескольких версиях Python и ОС:

name: Ruff Matrix

on: [push, pull_request]

jobs:
  ruff:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.10", "3.11", "3.12"]
      fail-fast: false

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

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

      - name: Install Ruff
        run: pip install ruff

      - name: Run Ruff
        run: ruff check --output-format=github .

Параметры матрицы:

  • os — три операционные системы (Ubuntu, macOS, Windows)
  • python-version — три версии Python (3.10, 3.11, 3.12)
  • fail-fast: false — если одна комбинация упала, остальные продолжают работу

Итого будет запущено 3 × 3 = 9 параллельных задач. Это может быть избыточно для Ruff (который проверяет статический анализ, не зависящий от версии Python), но полезно, если вы хотите убедиться, что код работает во всех окружениях.

💡 Для Ruff необязательно гонять на всех версиях Python — статический анализ не зависит от интерпретатора. Но matrix полезен, если вы дополнительно запускаете тесты или mypy в том же workflow.

Более практичный пример — матрица с разными наборами правил Ruff:

name: Ruff with Rule Sets

on: [pull_request]

jobs:
  ruff:
    strategy:
      matrix:
        ruleset:
          - "E,F"        # pycodestyle + Pyflakes
          - "I"          # isort
          - "N"          # pep8-naming
          - "UP,B,S"     # pyupgrade + bugbear + bandit

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: --select ${{ matrix.ruleset }} --output-format=github

Такой подход позволяет:

  • Изолировать ошибки по категориям правил
  • Быстро понять, какая группа правил вызывает проблемы
  • Параллельно запускать проверки — общее время меньше
🔄

Раздельные workflow для lint и format

Линтинг и форматирование — разные задачи, и их можно (а иногда и нужно) запускать в отдельных workflow. Вот несколько причин для разделения:

  • Разные триггеры — проверку форматирования можно запускать реже
  • Разные требования к версиям — lint может требовать определенного набора правил
  • Параллелизм — задачи выполняются одновременно, сокращая общее время
  • Читаемость логов — проще найти проблему в отдельном workflow

Workflow для линтинга (.github/workflows/lint.yml)

name: Lint

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

jobs:
  ruff-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: check --output-format=github

Workflow для форматирования (.github/workflows/format.yml)

name: Format

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

jobs:
  ruff-format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: format --check

Также можно объединить обе задачи в одном workflow, но в разных jobs — это даёт параллельный запуск и чистый интерфейс:

name: Ruff

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: check --output-format=github

  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: format --check
⚠️ ruff format --check не изменяет файлы — он только проверяет, что код уже отформатирован. Если вы используете ruff format (без --check), CI изменит файлы, но не закоммитит их — проверка пройдёт, но локальный код останется неотформатированным. Лучше использовать --check и требовать форматирования локально.
🚦

Exit коды и их значение в CI

GitHub Actions определяет успешность шага по exit code — коду завершения процесса. Ruff использует следующие exit коды:

Exit Code Значение Результат CI
0 Успех — ошибок не найдено ✅ Прошёл
1 Найдены ошибки линтинга ❌ Провал
2 Внутренняя ошибка Ruff ❌ Провал

Как это работает в CI:

  • Exit 0 — всё чисто, workflow помечается зелёной галочкой
  • Exit 1 — найдены ошибки, workflow помечается красным крестом. PR нельзя смержить (если настроены branch protection rules)

Если вам нужно игнорировать exit code Ruff (например, в переходный период, когда вы постепенно внедряете правила), используйте continue-on-error: true:

- name: Lint
  continue-on-error: true
  run: ruff check --output-format=github .

- name: Format check
  continue-on-error: true
  run: ruff format --check .

С continue-on-error: true workflow продолжит работу, даже если Ruff найдёт ошибки. Шаг будет отмечен жёлтым предупреждением, но не заблокирует PR. Это полезно на этапе внедрения Ruff в существующий проект.

💾

Кеширование Ruff в CI

Кеширование позволяет ускорить повторные запуски workflow, сохраняя бинарник Ruff или виртуальное окружение между запусками. GitHub Actions предоставляет встроенное кеширование через actions/cache.

Кеширование бинарника Ruff через actions/cache

name: Lint with Caching

on: [push, pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache Ruff
        uses: actions/cache@v4
        id: cache-ruff
        with:
          path: ~/.cache/ruff
          key: ${{ runner.os }}-ruff-${{ hashFiles('**/ruff.toml', '**/pyproject.toml') }}
          restore-keys: |
            ${{ runner.os }}-ruff-

      - uses: astral-sh/ruff-action@v3

Как работает кеширование Ruff:

  • Ruff хранит кеш анализа в ~/.cache/ruff
  • При повторном запуске Ruff проверяет, изменились ли файлы
  • Если файлы не изменились — Ruff использует кеш и завершается за миллисекунды
  • Ключ кеша включает хеш конфигурационных файлов — при изменении настроек кеш сбрасывается
💡 Ruff автоматически кеширует результаты анализа на уровне файлов. Повторный запуск без изменений файлов занимает миллисекунды. Кеширование в CI особенно полезно при большом количестве файлов.

Кеширование виртуального окружения с pip

Если вы устанавливаете Ruff через pip, можно кешировать виртуальное окружение:

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
    restore-keys: |
      ${{ runner.os }}-pip-

- name: Install Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.12"

- name: Install Ruff
  run: pip install ruff

Кеширование uv

Если вы используете uv, кеширование выполняется автоматически через astral-sh/setup-uv, но можно настроить и вручную:

- name: Cache uv
  uses: actions/cache@v4
  with:
    path: ~/.cache/uv
    key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }}
    restore-keys: |
      ${{ runner.os }}-uv-

- uses: astral-sh/setup-uv@v4
💡

Продвинутые сценарии

Запуск только на изменённых файлах

На больших проектах может быть полезно запускать Ruff только на изменённых файлах в PR, а не на всём проекте. Это ускоряет CI. Можно использовать tj-actions/changed-files для получения списка изменённых файлов:

name: Lint Changed Files

on: [pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed Python files
        id: changed-files
        uses: tj-actions/changed-files@v45
        with:
          files: |
            **/*.py

      - uses: astral-sh/ruff-action@v3
        with:
          src: ${{ steps.changed-files.outputs.all_changed_files }}

Проверка с фиксацией ошибок

Можно настроить workflow так, чтобы Ruff автоматически исправлял ошибки и коммитил изменения:

name: Auto-fix with Ruff

on:
  pull_request:
    branches: [main]

jobs:
  auto-fix:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}

      - uses: astral-sh/ruff-action@v3
        with:
          args: check --fix

      - uses: astral-sh/ruff-action@v3
        with:
          args: format

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "style: auto-fix with Ruff"
          branch: ${{ github.head_ref }}
⚠️ Автоматический коммит исправлений требует осторожности. Убедитесь, что права доступа настроены правильно (contents: write), иначе коммит не будет создан. Также не забудьте, что --fix исправляет только безопасные ошибки — некоторые требуют ручного вмешательства.

Отдельный workflow для разных веток

Можно настроить разные правила для разных веток — например, на main строгие правила, а на develop — более мягкие:

name: Strict Lint

on:
  push:
    branches: [main]

jobs:
  strict:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: check --output-format=github --select ALL
name: Basic Lint

on:
  push:
    branches: [develop]

jobs:
  basic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: check --output-format=github --select E,F

На main запускаются все правила (--select ALL), а на develop — только базовые (--select E,F). Это позволяет постепенно наводить порядок.

✅

Лучшие практики CI/CD с Ruff

📋 Рекомендации:

  1. Используйте astral-sh/ruff-action — это официальный, поддерживаемый и наиболее простой способ интеграции.
  2. Всегда включайте --output-format=github — аннотации в PR значительно упрощают код-ревью.
  3. Запускайте lint и format в отдельных jobs — для параллелизма и чистоты логов.
  4. Проверяйте только изменённые файлы на больших проектах — это ускоряет CI.
  5. Используйте branch protection rules — настройте GitHub так, чтобы PR нельзя было смержить без прохождения Ruff.
  6. Кешируйте бинарник Ruff и его кеш анализа — это ускоряет повторные запуски.
  7. Не используйте ruff format без --check — CI не должен изменять код, это задача разработчика.

📌 Что важно запомнить:

  • Ruff в CI — финальная линия обороны от некачественного кода
  • astral-sh/ruff-action@v3 — официальный Action
  • --output-format=github — аннотации в Pull Request
  • uvx ускоряет запуск через astral-sh/setup-uv
  • Матричный запуск полезен для кросс-платформенной проверки
  • Exit code 0 = успех, exit code 1 = найдены ошибки
  • Раздельные workflow для lint и format упрощают отладку
  • Кеширование ускоряет повторные запуски на 50-90%
📝

Пошаговая настройка: от нуля до production

Давайте пройдём полный путь настройки Ruff в GitHub Actions — от создания файла до production-готового workflow.

Шаг 1: Создайте структуру директорий

mkdir -p .github/workflows
touch .github/workflows/lint.yml

Шаг 2: Напишите базовый workflow

name: Ruff

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: check --output-format=github
  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3
        with:
          args: format --check

Шаг 3: Закоммитьте и отправьте в GitHub

git add .github/workflows/lint.yml
git commit -m "ci: add Ruff linter and formatter"
git push

Шаг 4: Проверьте результат

Откройте вкладку Actions в вашем репозитории на GitHub. Вы должны увидеть запущенный workflow «Ruff». Если код чист — он будет зелёным. Если есть ошибки — красным.

Шаг 5: Настройте branch protection (опционально)

Чтобы запретить мёрж PR с ошибками Ruff, перейдите в Settings → Branches → Add branch protection rule. Включите «Require status checks to pass before merging» и выберите «Ruff / lint» и «Ruff / format».

📊

Сравнение подходов к установке

Критерий ruff-action pip install uvx
Время установки ~2 с ~10-15 с ~3 с
Кеширование бинарника ✅ Встроенное ⚠️ Через actions/cache ✅ Встроенное
Зависимость от Python Нет Да Нет
Контроль версии Через параметр version Через requirements.txt Через параметр uvx
Простота настройки Максимальная Средняя Высокая
🐛

Типичные проблемы и их решение

❌ Workflow не запускается

Убедитесь, что файл находится в .github/workflows/lint.yml (правильный путь и расширение .yml, не .yaml если настроено иначе). Проверьте синтаксис YAML через валидатор.

❌ Ruff не устанавливается через ruff-action

Проверьте версию Action — используйте @v3. Убедитесь, что runner имеет доступ к GitHub API (не корпоративный прокси без доступа).

❌ Аннотации не появляются в PR

Убедитесь, что вы используете --output-format=github. Если workflow запускается от fork PR, аннотации могут не работать из-за ограничений GitHub Actions на токены от fork-репозиториев.

❌ ruff format --check всегда зелёный

Проверьте, что вы передаёте правильный путь к файлам. ruff format --check . проверяет все Python-файлы в текущей директории. Если файлы скрыты exclude — они не проверяются.

❌ Матричный запуск слишком долгий

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

📋

Что дальше

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

📌 Ключевые выводы:

  • Используйте astral-sh/ruff-action@v3 для максимальной простоты
  • Флаг --output-format=github даёт аннотации в Pull Request
  • Для проектов на uv используйте astral-sh/setup-uv и uvx ruff
  • Разделяйте lint и format на отдельные jobs
  • Матрица полезна для ОС, но не для версий Python (Ruff статический)
  • Exit code 0 = успех, 1 = ошибки
  • Кеширование ускоряет повторные запуски
  • Branch protection rules защищают main от плохого кода

Урок 6.3: GitHub Actions

5 вопросов