GitHub Actions запускает тесты, сборку и деплой автоматически — по событию в репозитории, без вашего участия. Скорее всего, сейчас у вас так: код лежит на GitHub, а всё остальное — прогон тестов, сборка, заливка на сервер — вы делаете руками, каждый раз одинаково и каждый раз с шансом что-то забыть. К концу этой статьи у вас будет работающий пайплайн: он сам гоняет тесты на каждый пуш и катит релиз в прод, стоит вам только смерджить код. Ручной прогон тестов и деплой перед каждым мержем съедает 10–15 минут — при десятке пушей в неделю это больше двух часов, которые можно потратить на что угодно, кроме копирования одних и тех же команд в терминал.
Если у вас уже собран базовый набор инструментов разработчика, GitHub Actions логично добавить туда следующим шагом — это часть той же автоматизации, которая экономит время, только на уровне не редактора кода, а целого репозитория.
ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
Что делает GitHub Actions
GitHub Actions — сервис автоматизации, встроенный в GitHub: он запускает нужные действия по событиям в репозитории — пуш, пул-реквест, создание тега или просто расписание. По сути это движок для двух связанных практик: непрерывной интеграции и непрерывной доставки.
Смысл проще увидеть на бытовом цикле, который проходит любой коммит: пуш → сборка → тесты → выкатка. Без автоматизации каждый шаг этого цикла кто-то делает руками, и пока в проекте один разработчик и десяток коммитов в месяц, это нормально. Как только людей и коммитов становится больше, ручные шаги начинают теряться — кто-то забыл прогнать тесты, кто-то задеплоил не ту ветку. GitHub Actions встаёт как раз на место этих ручных шагов: получает событие, поднимает виртуальную машину, выполняет заданную последовательность команд и сообщает результат — зелёным или красным.
Для большинства проектов на GitHub этого достаточно: конфиг лежит в том же репозитории, что и код, а раннеры поднимает сам GitHub. Команда переходит на GitLab CI или Jenkins обычно по трём причинам. Первая — монорепа на сотни сервисов, где нужны более гибкие условия запуска джобов, чем дают path-фильтры Actions. Вторая — требование закрытого контура: если пайплайн вообще не должен выходить в интернет, Jenkins на своём сервере подходит с самого начала, а не через настройку self-hosted runner поверх облачного сервиса. Третья — уже существующий парк своих серверов, который команда хочет использовать по максимуму, а не платить за облачные раннеры GitHub.
| Критерий | GitHub Actions | GitLab CI | Jenkins |
| Где живёт | SaaS на GitHub, конфиг в репозитории | SaaS или self-host, встроен в GitLab | Только self-host, разворачивается отдельно |
| Порог входа | YAML в репозитории, готовые экшены в Marketplace | YAML в репозитории, готовые темплейты | Настройка через UI и плагины, порог выше |
| Монорепа на сотни сервисов | Есть paths-фильтры, но лимиты по job заметны на масштабе | Гибче за счёт child pipelines и rules | Полный контроль, но конфигурация вручную |
| Закрытый контур без интернета | Нужен self-hosted runner поверх облачного сервиса | Есть self-hosted вариант GitLab целиком | Изначально рассчитан на закрытый контур |
Разница на практике не в том, что один инструмент лучше другого, а в том, где уже живёт инфраструктура команды. Если репозиторий на GitHub, а закрытого контура и монорепы на сотни сервисов нет — начинать разумно с Actions: что такое Jenkins и зачем он в CI/CD можно изучить отдельно, когда проект дорастёт до вопроса выбора инструмента.
Из чего состоит workflow
Прежде чем писать первый workflow GitHub Actions, стоит разобраться в четырёх сущностях, из которых он состоит — иначе даже простой YAML-файл дальше по статье будет читаться как малопонятный набор строк.
События и триггеры
Секция on определяет, какое событие запустит workflow. push срабатывает на каждый пуш в указанные ветки, pull_request — на открытие и обновление пул-реквеста, schedule — по расписанию в формате cron, а workflow_dispatch добавляет кнопку ручного запуска во вкладке Actions. У push и pull_request можно ограничить триггер конкретными ветками через branches или конкретными путями через paths — это пригодится дальше, в разделе про ускорение пайплайна для монорепы.
# события, которые запускают workflow
on:
# срабатывает при пуше в ветку main
push:
# ограничиваем триггер конкретной веткой
branches: [main]
# и на каждое обновление пул-реквеста в любую ветку
pull_request:
Типичная ошибка новичка — не ограничить push веткой и получить двойной прогон: один раз при пуше в ветку с фичей, второй раз при пуше в main после мержа. Если оба прогона не нужны, branches решает вопрос одной строкой.
Раз уж речь зашла про события в репозитории, рядом обычно всплывает вопрос, чем git pull отличается от git fetch: это разные операции с историей, а для триггеров Actions важно помнить одно — workflow видит именно то состояние репозитория, которое было запушено, а не то, что лежит у вас локально.
Джобы и раннеры
Job — единица выполнения внутри workflow: у каждой джобы свой раннер, виртуальная машина, поднятая с нуля. runs-on: ubuntu-latest, windows-latest или macos-latest указывает, какой образ использовать. За каждым latest стоит конкретная версия — например, Ubuntu 24.04, — но какая именно, GitHub может поменять без предупреждения при обновлении образов. Джоба, которая вчера собиралась, сегодня может упасть из-за того, что в новом образе поменялась версия системной библиотеки.
jobs:
test:
# ubuntu-latest -- самый быстрый и дешёвый вариант раннера
runs-on: ubuntu-latest
Если стабильность важнее актуальности пакетов в образе, latest можно заменить на конкретную версию — ubuntu-24.04 вместо ubuntu-latest. Это возвращается отдельным пунктом в разделе про частые ошибки новичков.
Шаги и готовые экшены
Step — самая мелкая единица внутри джобы, и у каждого шага один из двух режимов: run выполняет произвольную shell-команду, uses подключает готовый экшен из GitHub Marketplace. Экшены — это переиспользуемые блоки: скачать код (actions/checkout), поставить рантайм (actions/setup-node), закэшировать зависимости и так далее.
steps:
# run -- произвольная команда в shell
- run: npm ci
# uses -- готовый экшен по имени и версии
- uses: actions/checkout@v7
Версию экшена обычно закрепляют тегом (@v7), но для чувствительных к безопасности случаев — экшенов от сторонних авторов, получающих доступ к секретам, — тег можно подменить, если репозиторий экшена скомпрометируют. Надёжнее пинить экшен по конкретному SHA-коммиту: тогда подменить код без вашего ведома не получится, даже если тег переедет на другой коммит.
Переменные окружения и контексты
Внутри workflow доступны контексты — объекты с данными о текущем прогоне. github содержит информацию о событии и репозитории (github.sha, github.ref), runner — об окружении раннера, env — переменные окружения, которые вы задали сами. Обратиться к значению из контекста можно через синтаксис ${{ }}.
steps:
# подставляем в текст SHA текущего коммита из контекста github
- run: echo "Собираем коммит ${{ github.sha }}"
Частая путаница новичка — забыть, что ${{ }} работает только там, где GitHub Actions ожидает выражение (например, в with или env), а не как универсальная замена внутри произвольного текста shell-скрипта.
Большая скидка — 16% на все курсы Практикума
Если вы читаете эту статью, тогда вы точно разбираетесь в технологиях. Стать лучше и зарабатывать больше можно после курсов Практикума — по программированию, анализу данных и искусственному интеллекту.
До 30 сентября на все курсы действует скидка 16%, она применится автоматически при оплате. Потом цены станут выше, поэтому не откладывайте!
Если не хватает уверенности читать и писать тесты и конфиги на JS и Node.js — берите Бэкенд на Node.js для фронтенд-разработчиков; монорепа разрослась настолько, что paths-фильтры внутри одного workflow больше не спасают, и пора резать её на отдельные сервисы, — Микросервисная архитектура: расширенная версия; а если зона ответственности давно включает не только сам пайплайн, но и то, что происходит после неудачного деплоя, — SRE — обеспечение надёжности систем.
Первый workflow за десять минут
Дальше — практика. Чтобы настроить CI/CD в GitHub Actions с нуля, не нужно ничего устанавливать локально: весь процесс укладывается в пять шагов.
- Создайте в репозитории папку
.github/workflows/, если её ещё нет — GitHub ищет конфиги workflow строго по этому пути. - Внутри создайте файл
ci.ymlи вставьте минимальный конфиг ниже. - Закоммитьте и запушьте изменение в любую ветку — событие
pushиз секцииonтут же запустит workflow. - Откройте вкладку Actions репозитория: там появится прогон с названием CI и статусом «в процессе» или уже «успешно».
- Если прогон упал, откройте его и разверните джобу
test— лог покажет, на каком шаге и с какой ошибкой всё остановилось.
# имя workflow, отображается во вкладке Actions
name: CI
# события, которые запускают workflow
on:
# запуск при пуше в любую ветку
push:
# запуск при открытии или обновлении пул-реквеста
pull_request:
# список джобов, которые выполнит workflow
jobs:
# джоба с произвольным названием test
test:
# образ раннера -- свежий Ubuntu от GitHub
runs-on: ubuntu-latest
# последовательность шагов внутри джобы
steps:
# шаг 1: скачиваем код репозитория на раннер
- name: Checkout
uses: actions/checkout@v7
# шаг 2: ставим Node.js нужной версии
- name: Setup Node.js
uses: actions/setup-node@v7
with:
# версия Node.js для раннера
node-version: '22'
# шаг 3: устанавливаем зависимости проекта
- name: Install dependencies
run: npm ci
# шаг 4: запускаем тесты
- name: Run tests
run: npm test
Мы пишем эту статью в сентябре 2026, когда актуальные мажорные версии actions/checkout и actions/setup-node — v7. В части старых гайдов ещё встречается v5 или даже v4: версии обновились дважды за лето, и в логе таких прогонов GitHub уже показывает предупреждение об устаревшем рантайме Node.js — про это отдельно в разделе про частые ошибки.
Если всё прошло гладко, во вкладке Actions будет зелёная галочка напротив прогона, а внутри — четыре пройденных шага: checkout, установка Node.js, установка зависимостей и тесты. Это уже рабочий CI: тесты гоняются сами при каждом пуше, и дальше в статье этот же файл обрастёт кэшем, матрицей версий и деплоем.
Тесты, линтер и сборка в одном пайплайне
Один шаг npm test из предыдущего раздела — только начало. Пайплайн для автоматического запуска тестов в боевом проекте обычно устроен так: линтер проверяет стиль кода, юнит-тесты и автотесты проверяют логику, сборка собирает финальный билд — и каждый из этих шагов может упасть независимо от остальных.
jobs:
test:
runs-on: ubuntu-latest
steps:
# шаг 1: скачиваем код репозитория
- uses: actions/checkout@v7
# шаг 2: ставим Node.js
- uses: actions/setup-node@v7
with:
node-version: '22'
# шаг 3: ставим зависимости
- run: npm ci
# шаг 4: линтер отдельным шагом -- если упадёт, тесты дальше не побегут
- name: Lint
run: npm run lint
# шаг 5: тесты
- name: Test
run: npm test
# шаг 6: сборка -- раз тесты прошли, можно собирать билд
- name: Build
run: npm run build
Если какой-то шаг некритичен и не должен останавливать весь пайплайн — например, линтер настроен мягко и вы пока не готовы блокировать мерж из-за стиля кода, — добавьте continue-on-error: true именно к этому шагу, а не отключайте линтер вовсе.
Кэш зависимостей
npm ci на голом раннере каждый раз скачивает зависимости заново — на среднем проекте это лишние 30–60 секунд на каждый прогон. actions/cache сохраняет папку между прогонами и восстанавливает её по ключу:
# кэшируем папку с зависимостями npm
- uses: actions/cache@v4
with:
# какую папку кладём в кэш
path: ~/.npm
# ключ кэша меняется вместе с package-lock.json
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
У setup-node то же самое встроено из коробки — достаточно указать cache: npm внутри with, и экшен сам разберётся с ключами. actions/cache гибче: подходит для любых зависимостей, не только npm, и позволяет кэшировать что угодно — от pip до слоёв Docker.
Артефакты сборки
Артефакт — файл или папка, которую одна джоба передаёт другой (или просто сохраняет для скачивания вручную). Билд из джобы test логично передать в джобу deploy, а не пересобирать заново:
# сохраняем папку dist как артефакт для следующей джобы
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
По умолчанию GitHub хранит артефакт 90 дней, но это можно сократить параметром retention-days — про экономию на этом отдельно в разделе про ускорение пайплайна. Скачать артефакт в соседней джобе можно через actions/download-artifact@v8 — версии этих двух экшенов разошлись, потому что download-artifact ушёл дальше по номерам из-за собственных изменений.
Статус-чек в Pull Request
Чтобы красный пайплайн физически не давал смерджить пул-реквест, в Settings → Branches нужно включить required status checks и выбрать джобу CI. После этого в интерфейсе PR появляется блок с результатом прогона, и кнопка Merge остаётся неактивной, пока пайплайн не позеленеет.
Это одна из тех настроек, которую легко забыть: сам workflow можно написать идеально, но без required status check ничего не помешает смерджить ветку с падающими тестами — статус-чек просто останется декоративным индикатором.
Матрица версий и параллельные джобы
Если библиотека или приложение должны работать на нескольких версиях Node.js или на нескольких операционных системах, гонять тесты для каждой комбинации вручную не вариант. Здесь на сцену выходит матрица GitHub Actions: она запускает один и тот же набор шагов для каждой комбинации значений, которые вы перечислите через strategy.
jobs:
test:
strategy:
# не останавливаем всю матрицу, если упал один прогон
fail-fast: false
# ограничиваем число одновременно запущенных job'ов
max-parallel: 4
matrix:
# три версии Node.js для проверки совместимости
node-version: ['18', '20', '22']
# две операционные системы
os: [ubuntu-latest, windows-latest]
# сочетание сверх декартового произведения списков выше
include:
- os: macos-latest
node-version: '22'
# сочетание, которое не имеет смысла тестировать
exclude:
- os: windows-latest
node-version: '18'
# раннер джобы берётся из текущего элемента матрицы
runs-on: ${{ matrix.os }}
steps:
# скачиваем код
- uses: actions/checkout@v7
# ставим версию Node.js из текущей комбинации матрицы
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
# ставим зависимости
- run: npm ci
# гоняем тесты
- run: npm test
include добавляет отдельную комбинацию сверх декартового произведения — в примере это macOS с Node 22, которого не было бы при простом перемножении списков node-version и os. exclude, наоборот, убирает ненужное сочетание — здесь это Windows с Node 18. fail-fast: false говорит матрице не останавливать все шесть job’ов, если упал один, а max-parallel ограничивает, сколько job’ов крутится одновременно — полезно, если раннеров или доступных минут не так много.
За удобство приходится платить минутами: вместо одной джобы теперь шесть, и не все они по цене одинаковы. Windows считается против квоты бесплатных минут с множителем 2x, macOS — 10x, поэтому шесть job’ов из strategy.matrix обходятся заметно дороже, чем кажется на первый взгляд.
На практике это значит, что перед добавлением macOS или Windows в матрицу стоит спросить себя, действительно ли тесты валятся из-за платформы, а не просто «на всякий случай». Одна лишняя ОС в матрице множит счёт быстрее, чем кажется на первый взгляд.
Секреты, доступы и OIDC
Раздел про секреты GitHub Actions в большинстве гайдов сводится к одному абзацу: «добавьте секрет в настройках и обратитесь к нему через secrets.NAME». На практике вопросов больше, и часть из них всплывает уже в проде — например, почему pull_request от форка вдруг не получает секреты и ломает пайплайн, который до этого работал нормально.
Секреты репозитория, окружения и организации
Секрет можно добавить на трёх уровнях: в самом репозитории (Settings → Secrets and variables → Actions), в конкретном environment (доступен только джобам, привязанным к этому environment) или на уровне организации (доступен нескольким репозиториям сразу — удобно для общих токенов). Общие принципы того, как защитить API-ключи от утечки, работают и здесь: секрет не должен попадать в код, в логи и в артефакты сборки.
GitHub автоматически маскирует значение секрета в логах: если попробовать вывести его через echo, в логе вместо значения будет ***. Это защита от случайной утечки, а не от намеренной — секрет, записанный в файл и потом закоммиченный, замаскирован не будет, потому что маскировка работает только со стандартным выводом джобы.
OIDC вместо долгоживущих ключей
Хранить в секретах долгоживущий access key от AWS или GCP — рабочий, но не самый безопасный вариант: ключ не истекает сам, и если он утечёт, доступ остаётся открытым, пока кто-то не заметит и не отзовёт его вручную. OIDC решает это иначе: workflow запрашивает у GitHub короткоживущий токен, обменивает его на временные учётные данные облака, и через несколько минут эти данные сами становятся бесполезны.
jobs:
deploy:
runs-on: ubuntu-latest
# без явного id-token workflow не получит OIDC-токен
permissions:
id-token: write
contents: read
steps:
# скачиваем код
- uses: actions/checkout@v7
# обмениваем OIDC-токен на временные креды AWS
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
# роль в AWS, которая доверяет токену от GitHub
role-to-assume: arn:aws:iam::123456789012:role/gh-actions-deploy
# регион для последующих AWS-команд
aws-region: eu-central-1
# деплоим, используя временные креды
- name: Deploy
run: aws s3 sync ./dist s3://my-bucket
С 23 апреля 2026 у OIDC-токенов GitHub появился отдельный нюанс: subject claim — поле, по которому облако проверяет, что токен пришёл именно от вашего репозитория, — стал доступен в неизменяемом формате: в него добавили постоянные числовые ID организации и репозитория вместо одних только имён. Сделано это для того, чтобы имя нельзя было переиспользовать — если организацию переименуют или удалят, а освободившееся имя заберёт кто-то другой, старый trust policy в AWS не должен внезапно начать доверять чужим токенам. Сначала это был опциональный переключатель в настройках OIDC, а с 15 июля 2026 такой формат subject claim стал обязательным по умолчанию для всех новых и переименованных репозиториев. Trust policy, написанные до этой даты по старому формату — просто owner/repo без ID, — для новых репозиториев работать не будут, их придётся пересобрать под актуальный формат.
Права токена
У каждого прогона workflow есть встроенный GITHUB_TOKEN — временный токен, который GitHub выдаёт автоматически, без ручной настройки секретов. По умолчанию у него довольно широкие права, и если явно не ограничить их блоком permissions, джоба получит больше доступа, чем ей реально нужно.
permissions:
# только чтение кода -- для джобы test больше ничего не нужно
contents: read
Хорошая практика — прописывать permissions на уровне каждой джобы отдельно, а не полагаться на дефолт репозитория: тогда даже если один workflow скомпрометируют, урон ограничен правами именно этой джобы, а не всем, что мог бы GITHUB_TOKEN.
Деплой и окружения
Всё, что было в статье до сих пор, — это CI: тесты, сборка, проверки. Автодеплой GitHub Actions добавляет последний шаг — саму выкатку, и здесь у большинства проектов один из трёх сценариев.
Свой сервер по SSH
jobs:
deploy:
runs-on: ubuntu-latest
# джоба стартует только после успешного прохождения тестов
needs: test
steps:
# скачиваем код
- uses: actions/checkout@v7
# выполняем команды на удалённом сервере по SSH
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
with:
# адрес и доступы хранятся в секретах, а не в коде
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
# команды, которые выполнятся на сервере
script: |
cd /var/www/app
git pull origin main
npm ci --production
pm2 restart app
Облачный контейнерный сервис
Если приложение упаковано в Докер, деплой обычно выглядит как сборка образа и заливка его в реестр, откуда сервис сам подхватывает новую версию:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
# скачиваем код
- uses: actions/checkout@v7
# логинимся в реестр образов
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# собираем образ и заливаем его в реестр
- name: Build and push image
uses: docker/build-push-action@v6
with:
# контекст сборки -- корень репозитория
context: .
# заливаем собранный образ
push: true
# тег образа с номером коммита
tags: myorg/myapp:${{ github.sha }}
GitHub Pages
Для статических сайтов и фронтенд-проектов деплой встроен прямо в платформу:
jobs:
deploy-pages:
runs-on: ubuntu-latest
# права, нужные именно для деплоя на GitHub Pages
permissions:
pages: write
id-token: write
# environment привязывает джобу к настройкам Pages
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
# скачиваем код
- uses: actions/checkout@v7
# собираем статический сайт
- name: Build site
run: npm run build
# готовим билд к загрузке на Pages
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: ./dist
# публикуем на GitHub Pages
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
Во всех трёх сценариях деплой в прод стоит защитить через environments — отдельную сущность в Settings → Environments, которая добавляет ручное подтверждение, ограничение по веткам и таймер задержки перед стартом джобы.
jobs:
deploy-prod:
runs-on: ubuntu-latest
needs: test
# окружение production настроено в Settings → Environments
environment: production
steps:
# скачиваем код
- uses: actions/checkout@v7
# запускаем скрипт деплоя
- name: Deploy
run: ./scripts/deploy.sh
Required reviewers добавляет шаг, на котором джоба замирает и ждёт, пока кто-то из указанных людей нажмёт Approve — удобно, чтобы деплой в прод не случился из-за автоматического мержа. Wait timer работает иначе: задерживает старт джобы на заданное время без чьего-либо участия — этого бывает достаточно, чтобы успеть передумать или отменить прогон, если пайплайн запустился по ошибке.
Если деплой всё-таки прошёл неудачно, порядок действий такой:
- Остановить дальнейшие изменения — не мержить в main, пока прод не восстановлен.
- Найти последний рабочий тег в списке релизов.
- Запустить деплой заново с этим тегом вместо текущего main — вручную через
workflow_dispatchс указанием тега или через отдельный workflow отката. - Проверить, что прод действительно вернулся к рабочей версии — не только по коду, но и по факту: открыть сайт, прогнать smoke-тест.
- Разобраться, что сломалось, уже после того, как прод стабилен, а не в процессе тушения пожара.
Сколько стоит GitHub Actions
Стоимость GitHub Actions — тема, которую большинство русскоязычных гайдов обходят одним предложением про «бесплатные минуты». Ниже — несколько цифр, которые стоит прикинуть перед тем, как расставлять self-hosted runners или матрицы по всем джобам подряд. Данные приведены по состоянию на сентябрь 2026, после январского пересмотра цен: GitHub снизил ставки на облачные раннеры почти на 40% и заодно включил в них отдельный сбор за платформу, который раньше выставлялся сверху.
| Параметр | Значение |
| Linux | 0,006 USD за минуту |
| Windows | 0,010 USD за минуту |
| macOS | 0,062 USD за минуту |
| Free | 2 000 минут в месяц на приватные репозитории |
| Team | 3 000 минут в месяц |
| Enterprise | 50 000 минут в месяц |
| Публичные репозитории | без ограничения по минутам |
Команда из пяти человек на тарифе Team при расходе 15 000 минут в месяц: 3 000 минут входят в лимит бесплатно, остаются 12 000 минут overage. Если все прогоны на Linux — это около $72 в месяц (12 000 × $0,006); на практике счёт обычно чуть выше, потому что часть job’ов почти всегда крутится на Windows с множителем 2x к расходу квоты. Сборка под iOS на macOS длиной 15 минут стоит $0,93 за один прогон (15 × $0,062) — при десяти прогонах в день это почти $280 в месяц, и именно macOS чаще всего оказывается той статьёй расходов, которую никто не посчитал заранее.
Свои раннеры
Self-hosted runner — машина, которую вы сами подключаете к репозиторию вместо облачного раннера GitHub: свой сервер, VPS, даже ноутбук. Такой раннер бесплатен по минутам — платите только за железо, на котором он крутится, — и окупается, когда минут расходуется много, особенно на macOS. В декабре 2025 GitHub анонсировал отдельный платформенный сбор $0,002 за минуту для self-hosted раннеров, но после резкой реакции сообщества отложил его без объявленной новой даты. По состоянию на сентябрь 2026 self-hosted runner остаётся полностью бесплатным по минутам.
Главный риск self-hosted раннера — если он подключён к репозиторию, принимающему пул-реквесты от посторонних, чужой код в форке может выполниться прямо на вашей машине с доступом к вашей сети. Из соображений изоляции self-hosted runner для публичных репозиториев по умолчанию не запускает workflow из форков без ручного подтверждения — эту защиту стоит оставить включённой, если только вы не уверены на сто процентов в каждом контрибьюторе. Больше про изоляцию — в материале про виртуальную среду в Докере: те же принципы работают и для self-hosted раннера.
Как ускорить пайплайн
Оптимизация CI/CD обычно сводится к пяти приёмам, и большинство пайплайнов использует один-два максимум — хотя выигрыш от остальных ничуть не меньше.
Кэш зависимостей и слоёв Docker — самый очевидный приём:
# кэшируем зависимости npm по ключу лок-файла
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
Для Docker то же самое работает через встроенный в docker/build-push-action кэш: слои, которые не изменились, переиспользуются, а не пересобираются заново из Dockerfile.
concurrency с cancel-in-progress отменяет устаревшие прогоны:
# отменяем предыдущий незавершённый прогон на той же ветке
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Если запушить в одну ветку несколько раз подряд, каждый предыдущий прогон отменяется, как только стартует новый — вместо того, чтобы доработать вхолостую и списать минуты за результат, который уже никому не нужен.
Фильтр paths избавляет монорепу от лишних прогонов:
on:
push:
# запускаем workflow только если менялись файлы в этой папке
paths:
- 'backend/**'
Если в монорепе меняется только фронтенд, нет смысла гонять пайплайн бэкенда — paths фильтрует именно по изменённым файлам, а не по факту пуша как такового.
Разделение тяжёлой джобы на параллельные сокращает время ожидания:
jobs:
# раньше это была одна джоба lint-test-build
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
Одна джоба lint-test-build выполняется последовательно и суммирует время каждого шага. Три отдельные джобы стартуют одновременно на трёх раннерах — общее время прогона падает примерно до времени самого долгого из трёх шагов, а не до суммы всех.
Урезание retention-days у артефактов — приём другого рода:
- uses: actions/upload-artifact@v7
with:
path: dist/
# артефакт хранится 3 дня вместо 90 по умолчанию
retention-days: 3
Этот приём не ускоряет сам прогон — время выполнения от него не меняется. Он сокращает расход хранилища: артефакты по умолчанию живут 90 дней, и на активном проекте это быстро съедает бесплатный лимит storage, даже если минуты укладываются в тариф.
Частые ошибки новичков
Ошибки GitHub Actions почти всегда одни и те же — они кочуют из проекта в проект, потому что документация рассказывает про них вскользь, а набивать шишки лично не хочется никому.
Workflow вообще не запускается после пуша. Причина обычно в том, что файл лежит не в .github/workflows/, а просто в корне репозитория или в .github/. Правка: переместить файл строго в .github/workflows/ — GitHub ищет конфиги только там.
GitHub ругается на синтаксис, или джоба падает ещё до выполнения шагов. Чаще всего дело в сломанных отступах YAML — шаг сдвинут на один пробел не туда или вместо пробелов случайно попал таб. Правка: проверить файл валидатором YAML или встроенным редактором GitHub, который подсвечивает такие места, и выравнивать строго двумя пробелами.
В логе жёлтое предупреждение про Node.js 20, или экшен ведёт себя не так, как в документации. Причина — используется старая версия экшена (v3–v4), собранная под устаревший рантайм: раннеры массово переключились на Node 24 по умолчанию ещё 16 июня 2026, а 16 сентября 2026 Node 20 уберут с раннеров полностью. Правка: обновить экшены до актуальных мажорных версий — checkout и setup-node минимум до v7, они уже собраны под Node 24.
Workflow падает с ошибкой доступа при попытке запушить коммит или создать релиз из самого пайплайна. У GITHUB_TOKEN по умолчанию нет прав на запись, если явно не указан блок permissions. Правка: добавить в джобу permissions: contents: write (или другой нужный scope) — именно там, где это реально нужно, а не глобально на весь workflow.
Workflow запускает сам себя раз за разом, минуты улетают впустую. Workflow триггерится на push, и один из его шагов делает git push обратно в репозиторий — получается бесконечный цикл. Правка: исключить ветку или путь, в который пишет workflow, из триггера, либо добавить [skip ci] в сообщение автоматического коммита.
Шаг деплоя падает с ошибкой доступа именно в прогонах из форкнутых пул-реквестов. GitHub не передаёт секреты репозитория в workflow, запущенный триггером pull_request из форка, — это защита от кражи секретов через чужой код. Правка: не пытаться деплоить из pull_request на форк; для доверенных проверок использовать pull_request_target с осторожностью либо разделить пайплайн на тестовую и деплойную части.
Пайплайн, который вчера работал, сегодня падает без единого изменения в коде. Образ ubuntu-latest (или windows-latest, macos-latest) обновился, и в нём поменялась версия системного пакета, от которого зависел билд. Правка: закрепить конкретную версию образа — например, ubuntu-24.04 вместо ubuntu-latest — там, где стабильность важнее актуальности пакетов.
Чек-лист готового пайплайна
Прежде чем звать пайплайн готовым, пройдитесь по короткому чек-листу — он собирает в одном месте всё, что действительно важно для настройки CI/CD, а не только то, что бросается в глаза.
- Версии экшенов запинены (например,
@v7), а не оставлены на плавающемlatest. - Секреты не попадают в логи — нигде нет
echoилиprintс их значением. permissionsв каждой джобе ограничены минимально необходимым.- Кэш зависимостей настроен и реально используется — ключ кэша меняется вместе с лок-файлом.
- Статус-чек CI отмечен как required — без зелёного пайплайна мерж заблокирован.
- Деплой в прод требует подтверждения через environment с required reviewers.
- Есть понятный план отката на предыдущий тег, а не только общая уверенность, что «как-нибудь разберёмся».
- Расход минут хотя бы примерно прикинут и укладывается в лимит тарифа.
- Уведомления о падении пайплайна приходят в мессенджер, а не обнаруживаются на следующий день.
Частые вопросы
Бесплатны ли GitHub Actions для приватного репозитория? Да, но с лимитом: для приватных репозиториев доступно 2 000 минут в месяц на тарифе Free, 3 000 — на Team и 50 000 — на Enterprise, а публичные репозитории считают минуты без ограничений. После лимита минуты Linux стоят $0,006, Windows — $0,010, macOS — $0,062.
Чем GitHub Actions отличается от Jenkins? Главное отличие — где это работает: GitHub Actions запускается на облачных раннерах GitHub и настраивается YAML-файлом прямо в репозитории, а Jenkins разворачивается на своём сервере и настраивается через UI и плагины. Из-за этого Jenkins остаётся вариантом для закрытого контура без выхода в интернет, а GitHub Actions выигрывает по скорости старта — workflow можно запустить за десять минут без установки чего-либо.
Как запустить workflow вручную? Добавьте в секцию on триггер workflow_dispatch — после этого во вкладке Actions рядом с названием workflow появится кнопка Run workflow, которая запускает прогон без пуша или пул-реквеста.
Почему workflow не видит секрет? Чаще всего потому, что прогон запущен из пул-реквеста форка — GitHub не передаёт секреты репозитория в такие прогоны из соображений безопасности, чтобы чужой код не мог их прочитать. Вторая частая причина — опечатка в названии секрета или секрет добавлен не на том уровне, например, в другое environment.
Можно ли деплоить на сервер без публичного IP? Да, через self-hosted runner, который сам стучится наружу к GitHub — тогда серверу не нужен входящий доступ из интернета, а деплой происходит локально на машине, где runner установлен.
Заключение
Минимальный CI в GitHub Actions собирается за один вечер: один workflow, одна джоба, тесты на каждый пуш. Дальше пайплайн растёт вместе с проектом — сегодня добавляете кэш и матрицу версий, через месяц OIDC вместо голых ключей и environment с ручным подтверждением перед продом. Начинать стоит с одной джобы на тесты, а не пытаться сразу собрать пайплайн со всеми возможностями разом — GitHub Actions это позволяет, и именно поэтому с него удобно начинать автоматизацию, если раньше всё приходилось гонять руками.
Если после пайплайна хочется разобраться в теме шире — почитайте, как выглядит DevOps с нуля в 2026: CI/CD — только часть этой картины.
Советуем дополнительно почитать
Что такое API — самое базовое объяснение того, что вообще делает API, если раздел Основные команды Git: шпаргалка с примерами — для тех, кому в статье про CI/CD не хватило базовой уверенности в самом Git: ветки, коммиты, откат изменений.
DevSecOps: что это такое и как работает — Shift Left и встраивание проверок безопасности в каждый этап разработки, шире, чем один блок про секреты и OIDC.
Terraform для начинающих: инфраструктура как код — следующий логичный шаг после автоматизации пайплайна: описание кодом уже самой инфраструктуры, куда этот пайплайн деплоит.
SSH-подключение: что это и как использовать — как устроено защищённое подключение, на котором держится сценарий «Свой сервер по SSH» из статьи.
Запускаем Python-скрипт на сервере, чтобы он работал всё время — что делать после того, как деплой по SSH прошёл: как через systemd не дать процессу упасть после перезагрузки сервера.
Бонус для читателей
Если вам интересно погрузиться в мир IT и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой или безлимитом на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
