Как перестать отлаживать GitHub Actions через бесконечные коммиты
Знакомая история: правишь YAML-конфиг пайплайна, делаешь коммит с сообщением fix ci, пушишь, ждешь пару минут, ловишь красный крестик из-за опечатки в названии параметра. Следом идут коммиты fix ci 2, testing action, why it fails и finally working. Цикл обратной связи в CI/CD часто превращается в пытку просто потому, что синтаксис проверяется уже на удаленном раннере.
Я регулярно наступал на эти грабли, пока не наткнулся на actionlint. Это статический анализатор для workflow-файлов GitHub Actions, написанный на Go разработчиком под ником rhysd.
Утилита проверяет файлы локально за доли секунды и находит ошибки задолго до того, как код попадет в удаленный репозиторий.
Что обычно ломается в пайплайнах
Большинство линтеров для YAML просто проверяют базовый синтаксис разметки: отступы, кавычки, списки. Но валидный YAML вполне может быть абсолютно нерабочим пайплайном для GitHub Actions.
actionlint идет глубже и парсит саму семантику конфигураций. Вот типичный пример сомнительного воркфлоу из документации проекта:
on:
push:
branch: main
tags:
- 'v\d+'
jobs:
test:
strategy:
matrix:
os: [macos-latest, linux-latest]
runs-on: ${{ matrix.os }}
steps:
- run: echo "Checking commit '${{ github.event.head_commit.message }}'"
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node_version: 18.x
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }}
if: ${{ github.repository.permissions.admin == true }}
- run: npm install && npm test
На первый взгляд файл выглядит нормально. Обычный синтаксический чекер пропустит его без замечаний. Если натравить на него actionlint, линтер моментально выдаст сразу семь ошибок:
- Неверный ключ
branchвместоbranchesв секцииpush. - Попытка использовать регулярное выражение
\d+в фильтре тегов, где гитхаб принимает только glob-паттерны. - Несуществующая метка раннера
linux-latest(вместоubuntu-latest). - Потенциальная уязвимость script injection: текст коммита подставляется напрямую в inline-скрипт без экранирования через переменные окружения.
- Опечатка в параметре
node_version(экшенactions/setup-nodeждетnode-versionчерез дефис). - Обращение к несуществующему полю
matrix.platform, когда в матрице объявлена переменнаяos. - Ошибка типов в выражении условия
if.
Что умеет проверять утилита
Внутри actionlint заложено несколько независимых уровней анализа.
Строгая типизация контекстов и выражений
В GitHub Actions выражения вида ${{ ... }} вычисляются на лету. Ошибиться в них проще простого: обратиться к полю, которого нет в контексте события, или перепутать типы данных. actionlint знает схему контекстов GitHub Actions (github, env, matrix, steps и других) и валидирует типы выражений. Если попытаться вызвать метод у строки или обратиться к несуществующему выходу шага (steps.my_step.outputs.foo), линтер сразу об этом скажет.
Валидация популярных экшенов
Линтер умеет проверять соответствие входных параметров (with:) реальным спецификациям используемых экшенов. Он знает метаданные популярных действий из официального каталога и подтягивает их схему, чтобы подсветить лишние или неправильно названные параметры.
Проверка встроенных скриптов
Если в шаге run: написан bash-скрипт, actionlint вызывает ShellCheck для проверки логики команд, поиска незакавыченных переменных и типичных багов в шелл-скриптах. Если в шаге используется Python, линтер умеет подключать pyflakes. При этом утилита аккуратно мапит позиции ошибок внутри многострочных скриптов прямо на строки исходного YAML-файла.
Поиск проблем безопасности
Одна из самых неприятных уязвимостей в CI/CD — выполнение произвольного кода через данные из pull request или заголовков коммитов. Когда мы пишем run: echo "${{ github.event.pull_request.title }}", злоумышленник может внедрить в название PR точку с запятой и выполнить любую команду с правами раннера. actionlint отслеживает такие ненадежные источники данных и требует передавать их через переменные окружения.
Варианты запуска
Утилиту можно запускать разными способами в зависимости от привычного воркфлоу.
Самый простой вариант — поставить бинарник через Go:
go install github.com/rhysd/actionlint/cmd/actionlint@latest
После установки достаточно запустить команду в корне репозитория:
actionlint
Она сама найдет папку .github/workflows/ и проверит все вложенные манифесты.
Если не хочется ничего ставить на хост, проект доступен в виде Docker-образа, плагина для VS Code, хука для pre-commit и интеграции для утилиты reviewdog. Автор также собрал онлайн-песочницу на WebAssembly, где можно вставить свой YAML в браузере и посмотреть результаты анализа без терминала.
Настройка под свои нужды
В реальных проектах часто используются self-hosted раннеры со специфичными метками или кастомные переменные окружения. Чтобы линтер не ругался на неизвестные метки машин, достаточно создать конфигурационный файл .github/actionlint.yaml:
self-hosted-runner:
labels:
- gpu-runner
- arm-builder
В этом же конфиге можно отключать отдельные правила или задавать шаблоны игнорирования для конкретных файлов.
Кому пригодится
Если вы поддерживаете больше одного пайплайна в GitHub Actions, actionlint бережет нервы и экономит платные минуты раннеров. Утилита легкая, не требует настройки Node.js или тяжелых рантаймов, запускается моментально и закрывает 90% глупых опечаток еще на этапе git commit. Самый разумный шаг — повесить ее в pre-commit хук или отдельным шагом в pull request checks.
