CI: робот проверяет за тебя
Зачем это нужно
Самая дорогая фраза в разработке — «у меня локально работает». Она произносится ровно в тот момент, когда работа уже сдана, а у проверяющего она не собирается: не хватает файла, который забыли добавить в git; проект собирается только на твоей версии Node; тесты проходят, потому что в твоей папке лежит config.local.json, которого нет ни у кого больше. Локальная машина — это не «чистая» среда, а место, где за месяцы работы накопилось столько всего, что уже непонятно, чем именно ты держишься на плаву.
CI (continuous integration, непрерывная интеграция) решает эту проблему одним движением: каждый раз, когда ты отправляешь коммит, сервер поднимает чистую машину, забирает на неё твой репозиторий ровно в том виде, в каком он лежит в git, и прогоняет то, что ты попросишь — сборку, тесты, линтер. Одинаково. Всегда. Без «ой, у меня же ещё вчерашняя ветка была открыта».
Эта глава — про GitHub Actions: самый распространённый CI, встроенный прямо в GitHub, где ничего не нужно поднимать и настраивать снаружи. Дальше предполагается, что коммит, ветка, push и pull request — уже знакомые слова.
- Зачем это нужно
- Что робот проверяет такого, чего не проверишь сам
- Где живут workflow
- Минимальный workflow построчно
- actions/checkout: почему без него всё падает
- Настройка окружения и кэш
- Матрица версий
- Секреты: ключи не кладут в yml
- Артефакты и передача между job-ами
- Бейдж статуса в README
- Как читать красный лог
- Типичные ошибки
- Заводим CI на своём репозитории: пошагово
- Практика: заведи workflow руками
- На чемпионате
- Шпаргалка
- Что должен уметь
- Куда дальше: проверенные ресурсы
Что робот проверяет такого, чего не проверишь сам
Формально ничего сверхъестественного: те же самые команды, что ты набираешь в терминале. Разница — в среде, и именно она ловит ошибки.
- Незакоммиченный файл. Тесты на твоей машине читают
src/utils.py, который ты написал, но не сделалgit add. У робота этого файла нет — сборка падает сModuleNotFoundError. Дома такое не воспроизводится в принципе. - Забытая зависимость. Пакет установлен глобально год назад, но в
package.jsonего нет. У тебя импорт работает, на чистой машине —Cannot find module. - Версия рантайма. Локально Node 22, у проверяющего — 20. Синтаксис, который есть в одной версии и отсутствует в другой, ломает сборку молча и не там, где ждёшь.
- Регистр в имени файла. macOS и Windows не различают
Header.tsxиheader.tsx, Linux — различает. Импорт./headerживёт у тебя и падает на раннере. - Человеческий фактор. Устал, торопился, забыл прогнать тесты перед push. Робот не устаёт и не забывает.
Это не гипотетический список: каждая строка — это отдельная причина, по которой на чемпионате сдают проект, который «точно работал». Разница между «работал у меня» и «работает» измеряется одним зелёным значком у коммита.
CI ничего не чинит и ничего не улучшает сам по себе. Он делает ровно одно: превращает «наверное, работает» в «проверено на чистой машине». Если тестов в проекте нет, зелёная галочка означает только то, что проект собрался — не больше. Ценность CI ровно равна ценности того, что он запускает.
Где живут workflow
GitHub Actions ищет описания процессов ровно в одном месте: папка .github/workflows/ в корне репозитория, файлы с расширением .yml или .yaml. Один файл — один workflow (процесс). Файлов может быть сколько угодно: ci.yml для тестов, deploy.yml для выкладки, lint.yml для проверки стиля.
твой-репозиторий/
├── .github/
│ └── workflows/
│ ├── ci.yml ← тесты на каждый push
│ └── deploy.yml ← выкладка при пуше в main
├── src/
├── package.json
└── README.md
Почему именно там? Потому что это соглашение GitHub, и обойти его нельзя: папка .github — служебная (в ней же живут шаблоны issue и правила ревью), а workflows внутри неё сканируется автоматически. Положишь тот же файл в workflows/ci.yml или в .github/ci.yml — GitHub его просто не заметит, ошибки не будет, и «почему у меня ничего не запускается» превратится в двадцать минут поиска. Первая проверка при молчащем CI — глазами убедиться, что путь до файла ровно .github/workflows/имя.yml.
Важное следствие: workflow — часть репозитория, а значит, он проходит по всем правилам git. Его видно в diff, его ревьюят, его история хранится вместе с кодом, и на разных ветках он может быть разным. Правило запуска простое: GitHub берёт версию workflow из той ветки, куда пришёл коммит. Поправил ci.yml в своей ветке — правка сработает уже на этом пуше, ещё до вливания в main.
Минимальный workflow построчно
Вот файл, который делает ровно то, что нужно от первого CI: на каждый push и на каждый pull request поднимает чистый Linux, забирает код, ставит зависимости и прогоняет тесты.
name: ci
on:
push:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
Двадцать строк, и каждый ключ здесь важен. Разберём по порядку появления.
name: ci — имя workflow, которое видно во вкладке Actions и в списке проверок у pull request. Необязательный ключ (без него GitHub подставит имя файла), но с ним в интерфейсе понятнее: «ci упал» читается лучше, чем «.github/workflows/ci.yml упал».
on: — триггеры, то есть события, при которых workflow запускается. Здесь их три:
push— любой пуш в любую ветку репозитория. Можно сузить:push: branches: [main]— только пуши в main.pull_request— открытие PR и каждый новый коммит в него. Это то, что рисует галочку или крестик прямо в интерфейсе PR.workflow_dispatch— кнопка «Run workflow» во вкладке Actions: ручной запуск без всякого коммита. Стоит добавлять почти всегда — она бесплатна, а когда нужно перезапустить проверку без пустого коммита, спасает.
jobs: — список задач. Job — это независимая единица работы, которой выдаётся своя отдельная чистая машина. Здесь job один и называется test (имя произвольное, оно же попадёт в интерфейс).
runs-on: ubuntu-latest — какую машину поднять под этот job. Варианты: ubuntu-latest, windows-latest, macos-latest. Ubuntu — самая быстрая и дешёвая, и по умолчанию берут её; macOS нужен только тогда, когда без него нельзя (сборка под iOS).
steps: — список шагов внутри job, выполняются строго по порядку сверху вниз, на одной и той же машине. Если шаг завершился с ненулевым кодом возврата, job падает, и все следующие шаги пропускаются.
uses: против run: — вот это различие стоит запомнить намертво:
uses: actions/checkout@v4— подключить готовый чужой шаг (action), написанный кем-то и лежащий в отдельном репозитории. Формат —владелец/репозиторий@версия.@v4здесь — тег версии; писать@mainне стоит: чужой репозиторий обновится, и твой CI сломается в неожиданный день.run: npm test— выполнить свою команду в shell машины, ровно как в терминале. Всё, что ты набираешь руками, идёт черезrun.
Правило выбора простое: если задачу решает одна-две команды — run. Если задача типовая и нудная (скачать репозиторий, поставить Node нужной версии, залить артефакт) — почти наверняка на неё уже есть готовый action, и ищут его в GitHub Marketplace.
YAML не терпит табуляции: отступы делаются только пробелами. Редактор, который вставляет табы, ломает workflow с сообщением вида You have an error in your yaml syntax on line 7 — и это будет единственное, что ты увидишь, потому что до запуска дело не дойдёт. Вложенность в YAML задаётся отступом, а не скобками: два пробела на уровень — общепринятый шаг.
actions/checkout: почему без него всё падает
Самое частое недоумение первого дня: «я написал run: npm test, а он пишет, что нет файла». Причина в том, как устроен раннер.
Раннер — это виртуальная машина, которая создаётся под твой job с нуля и уничтожается после него. На ней есть операционная система и предустановленный набор инструментов (git, Node, Python, docker), но твоего кода на ней нет. GitHub не копирует репозиторий на раннер сам: это осознанное решение, потому что не каждому job вообще нужен код (бывают job-ы, которые только шлют уведомление или дёргают чужой API).
Забрать код — работа отдельного шага:
- uses: actions/checkout@v4
Он делает по сути git clone твоего репозитория в рабочую папку раннера и переключается на тот коммит, который вызвал запуск. Без него рабочая папка пуста, и первая же команда даёт что-то вроде:
npm error code ENOENT
npm error syscall open
npm error path /home/runner/work/my-project/my-project/package.json
npm error errno -2
npm error enoent Could not read package.json
Обрати внимание на путь: /home/runner/work/… — это рабочая папка раннера, и package.json в ней действительно нет. Сообщение честное, просто читается как «странная ошибка npm», хотя означает «ты забыл checkout».
Порядок тоже важен: checkout идёт первым шагом. Он не «настройка», а «привоз кода» — всё, что стоит выше него, работает в пустой папке.
Настройка окружения и кэш
Второй типовой шаг — поставить нужную версию рантайма. На раннере уже есть какая-то версия Node, но полагаться на неё нельзя: она меняется с обновлениями образа, и однажды твой проект соберётся не так, как вчера. Поэтому версию фиксируют явно:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
with: — параметры, которые передаются в action. У каждого action свой набор, и смотреть его нужно в README этого action.
node-version: 20 — какую версию поставить. Лучше указывать ту же мажорную версию, что и у проекта в разработке. Ещё лучше — не дублировать число в двух местах, а положить его в файл .nvmrc и написать node-version-file: .nvmrc.
cache: npm — включить кэш зависимостей. Без него каждый запуск качает все пакеты заново из интернета: на среднем проекте это минута-две в каждом запуске. С кэшем node_modules восстанавливается из сохранённого архива, и npm ci укладывается в секунды. Ключ кэша action считает сам по хешу package-lock.json: поменялся lock-файл — кэш промахнулся и зависимости ставятся заново; не поменялся — попадание.
Для Android и Kotlin логика ровно та же, меняется только action: нужна JDK, а не Node.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
- run: ./gradlew test
distribution — какая сборка JDK (temurin — стандартный бесплатный выбор), cache: gradle кэширует зависимости Gradle. Аналогично устроены setup-python, setup-go, setup-dotnet — семейство официальных action-ов с одинаковой логикой: версия + кэш.
npm ci против npm install
В CI ставят зависимости командой npm ci, а не привычной npm install, и это не вкусовщина.
npm install | npm ci | |
|---|---|---|
| Откуда берёт версии | из package.json, разрешая диапазоны заново | строго из package-lock.json |
| Может изменить lock-файл | да | нет, а при расхождении с package.json падает с ошибкой |
Что делает с node_modules | доставляет недостающее | удаляет папку целиком и ставит заново |
| Скорость на чистой машине | медленнее | быстрее |
Смысл в воспроизводимости. package.json обычно разрешает диапазон версий ("^4.17.0" — «любая 4.x не ниже 4.17.0»), и npm install в разные дни поставит разные версии. npm ci ставит ровно то, что записано в lock-файле, — значит, сборка сегодня и сборка через месяц идентичны. Побочная польза: если package-lock.json забыли закоммитить или он разошёлся с package.json, npm ci падает сразу и громко:
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json are in sync. Please update your lock file with
npm error `npm install` before continuing.
Это не поломка CI, а именно тот класс ошибок, ради которого CI и заводят: рассинхрон, который дома не виден вообще.
Матрица версий
Иногда проверить нужно не на одной версии, а на нескольких: «работает ли проект и на Node 18, и на Node 20, и на Node 22». Копировать job три раза не нужно — есть strategy.matrix:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
GitHub развернёт этот job в три параллельных: test (18), test (20), test (22) — каждый на своей машине. В логах видно, какая именно комбинация упала.
Два нюанса, которые экономят нервы:
fail-fast: false. По умолчанию GitHub отменяет остальные варианты матрицы, как только упал первый. Это быстро, но неинформативно: непонятно, сломано всё или только Node 18. Выключенныйfail-fastдаёт полную картину за одну итерацию.- Матрица умножается. Добавишь вторую ось (
os: [ubuntu-latest, windows-latest]) — получится не пять job-ов, а шесть: три версии на две системы. Матрица на четыре оси разворачивается в десятки запусков и съедает лимит бесплатных минут за неделю. Учебному проекту хватает одной оси, а часто и вовсе одной версии.
Синтаксис ${{ ... }} — это язык выражений GitHub Actions, а не YAML и не bash. Внутри доступны контексты: matrix (текущая комбинация), github (кто, куда и что запушил — например, github.ref с именем ветки), secrets, env, runner. Выражение вычисляется на стороне GitHub до запуска шага, поэтому значение подставляется в команду ещё до того, как shell её увидит.
Секреты: ключи не кладут в yml
Рано или поздно workflow понадобится что-то, чего не должно быть в репозитории: SSH-ключ для выкладки, токен API, пароль к базе тестового стенда. Класть их прямо в файл нельзя — и вот почему это серьёзнее, чем кажется.
Файл workflow лежит в git. Значит, ключ, попавший в него, оказывается: в истории коммитов (откуда его не убрать обычным удалением строки — старый коммит остаётся); у каждого, кто клонировал репозиторий; в публичном доступе, если репозиторий открыт; в чужих форках. Боты сканируют публичный GitHub на предмет ключей непрерывно — счёт идёт на минуты между «запушил токен» и «токен использован кем-то другим». Единственный настоящий способ починки после утечки — ротация: старый ключ отзывают, выпускают новый. Удаление строки не помогает.
Правильное место — Settings → Secrets and variables → Actions в настройках репозитория. Там два раздела:
- Secrets — то, что нельзя показывать: токены, ключи, пароли. Значение вводится один раз и после сохранения не показывается больше никому, включая владельца репозитория: его можно только заменить.
- Variables — обычные настройки, которые не секретны: имя окружения, адрес сервера, номер версии. Их видно в интерфейсе, и это нормально.
В workflow к ним обращаются через контексты secrets и vars:
- name: deploy
run: ./deploy.sh
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TARGET: ${{ vars.DEPLOY_HOST }}
Почему секрет не появляется в логе. GitHub знает значения всех секретов и при печати лога заменяет любое их вхождение на ***. Даже если ты по неосторожности напишешь run: echo $SSH_KEY, в логе окажется звёздочки. Это страховка, а не разрешение: маскирование работает по точному совпадению строки, и секрет, разрезанный пополам, закодированный в base64 или напечатанный по одному символу, замаскирован не будет. Правило прежнее — секреты не печатают.
Секреты не отдаются форкам. Pull request из чужого форка запускается без доступа к секретам репозитория: контекст secrets в нём пустой. Это защита от очевидной атаки — иначе любой желающий открыл бы PR с шагом «распечатай все секреты» и получил бы ключи к чужому серверу. Практическое следствие: workflow, который умеет только деплоить, на PR из форка будет падать, и это правильное поведение. Проверки (сборка, тесты) в такие workflow-ы кладут отдельно, без секретов, чтобы форки проверялись нормально.
Если ключ всё-таки уехал в репозиторий — считай его скомпрометированным с этой секунды. Порядок действий: сначала отозвать и перевыпустить ключ у того, кто его выдал (GitHub, хостинг, платёжный сервис), и только потом чистить историю. Обратный порядок бесполезен: пока старый ключ действует, неважно, видно его в истории или нет.
Артефакты и передача между job-ами
Каждый job получает свою чистую машину, и она уничтожается по его завершении. Отсюда два практических вопроса: как забрать результат сборки себе и как передать его следующему job-у. Ответ на оба один — артефакты.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci && npm test && npm run build
- uses: actions/upload-artifact@v4
with:
name: site
path: build
retention-days: 1
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: site
path: build
- run: ./upload-to-server.sh build
Это, с точностью до мелочей, реальный deploy.yml этой самой платформы. Разберём новые ключи:
upload-artifactупаковывает указанную папку (path: build) и складывает её на сторону GitHub под именемname: site. Скачать её можно руками со страницы запуска во вкладке Actions — удобно, когда нужно посмотреть глазами, что именно собралось.retention-days: 1— сколько дней хранить. По умолчанию 90; для промежуточной передачи между job-ами хватает одного дня, и это экономит квоту хранилища.needs: build— jobdeployждёт успешного завершения job-аbuild. Безneedsjob-ы стартуют одновременно, иdeployпобежит скачивать артефакт, которого ещё нет.needs— единственный способ задать порядок между job-ами.download-artifactраспаковывает артефакт на новую машину. Обрати внимание: папкуbuildво втором job-е не «видно из первого» — она физически появляется там только благодаря этой паре шагов.
Когда стоит разбивать на job-ы вместо одного длинного списка шагов: когда части независимы (линтер и тесты могут идти параллельно), когда нужны разные машины (сборка на Linux, проверка на Windows) или когда вторая часть должна выполняться не всегда. Если всё делается подряд на одной машине — не усложняй: один job, много шагов, никаких артефактов.
Бейдж статуса в README
Результат последнего запуска можно вывести картинкой прямо в README — это первое, что видит человек, открывший репозиторий:

Адрес собирается механически: владелец, репозиторий, дальше /actions/workflows/, имя файла workflow и /badge.svg. Картинка отдаётся GitHub-ом на лету и показывает состояние последнего запуска на ветке по умолчанию: passing зелёным или failing красным. Готовую разметку можно не набирать руками — во вкладке Actions у каждого workflow есть пункт меню «Create status badge», который копирует её в буфер.
Пара уточнений, чтобы бейдж не врал:
- Бейдж показывает ветку по умолчанию, а не ту, что открыта у тебя. Нужна другая — добавь
?branch=developк адресу. - Красный бейдж в README — это нормальная рабочая ситуация, а не позор. Ненормально — красный бейдж, который висит неделю, потому что на него перестали смотреть.
Как читать красный лог
Навык, ради которого стоит вся глава: workflow упал, и нужно за минуту понять почему. Разберём по шагам.
1. Открой запуск. Вкладка Actions, верхняя строка списка (или крестик прямо в PR — он кликабелен). Внутри — список job-ов слева.
2. Найди упавший шаг. Шаги перечислены сверху вниз, у каждого значок: зелёная галочка — прошёл, красный крестик — упал, серый — не выполнялся. Упавший шаг ровно один: после него всё остальное автоматически пропущено. Раскрой его — логи по умолчанию свёрнуты, кликнув по названию, получаешь весь вывод команды.
3. Ищи ПЕРВУЮ ошибку, а не последнюю. Это главное правило, и оно противоречит инстинкту. Взгляд сам летит в конец лога, где красным написано что-то финальное:
Error: Process completed with exit code 1.
Эта строка не значит ничего: она означает «команда завершилась неуспешно» и печатается всегда. То же самое с npm ERR! code ELIFECYCLE. Настоящая причина — выше, там, где первая ошибка. Прокрути вверх до места, где нормальный вывод сменяется руганью:
FAIL tests/cart.test.js
● корзина суммирует позиции
expect(received).toBe(expected)
Expected: 200
Received: 500
at Object.total (src/cart.js:42:18)
Tests: 1 failed, 23 passed, 24 total
npm ERR! Test failed. See above for details.
npm ERR! code ELIFECYCLE
Error: Process completed with exit code 1.
Полезного здесь ровно четыре строки в начале: какой файл, какой тест, что ожидалось и что получилось, в каком месте кода. Остальные пять — это эхо, которое каждый слой печатает поверх предыдущего.
4. Отличи падение теста от падения окружения. Это развилка, которая определяет, что чинить:
| Признак в логе | Что это | Куда смотреть |
|---|---|---|
FAIL tests/..., AssertionError, Expected … Received … | упал тест | код и сам тест: логика действительно сломана |
Cannot find module, ModuleNotFoundError, ENOENT, command not found | упало окружение | забыт файл в коммите, зависимость не в package.json, забыт checkout или setup |
You have an error in your yaml syntax | упал сам workflow | отступы и синтаксис yml — до кода дело не дошло |
Permission denied, 403, Bad credentials | упали права/секреты | секрет не задан, истёк, или это PR из форка |
The job running on runner … has exceeded the maximum execution time | таймаут | зависший процесс, ожидание ввода, бесконечный цикл |
Быстрый способ проверить гипотезу «сломано окружение, а не код»: клонировать репозиторий в новую пустую папку и прогнать те же команды. Если дома в чистом клоне тоже падает — дело точно в незакоммиченном файле или в зависимостях, а не в раннере.
5. Не гадай — добавь вывод. Если из лога непонятно, что вообще происходит, отладка в CI выглядит как обычная отладка печатью: временный шаг run: ls -la (что вообще лежит в папке), run: node --version (какая версия), run: cat package.json. Один такой пуш даёт больше, чем полчаса рассуждений о том, как оно там устроено.
Типичные ошибки
Шесть граблей, на которые наступают почти все — лучше здесь, чем на сдаче.
Табы в YAML. Отступ табуляцией — синтаксическая ошибка, и она не всегда очевидна глазами, потому что визуально таб похож на пробелы. Лечится настройкой редактора («вставлять пробелы вместо табов») и просмотром diff перед коммитом. Второй по частоте вариант той же ошибки — разъехавшиеся уровни вложенности: steps на одном уровне с runs-on, а не внутри job-а.
Не тот триггер или не та ветка. Написал on: push: branches: [main], работаешь в ветке feature/cart — и удивляешься тишине. GitHub ничего не скажет: нет подходящего события — нет запуска. Проверка простая: во вкладке Actions пусто, значит, дело в триггере. Для проверок обычно нужен и push, и pull_request.
Забытый checkout. Разобран выше, но повторяется он с завидной регулярностью — особенно когда в workflow добавляют второй job и не дублируют в нём checkout. Каждый job начинает с пустой машины: то, что первый job сделал checkout, второму не помогает ничем.
Тест, который проходит локально из-за незакоммиченного файла. Самый обидный случай, потому что «я же проверял». Файл конфигурации, тестовые данные, .env — что-то из этого лежит у тебя и попало в .gitignore (или просто не добавлено), а тест на него опирается. Проверка на пять секунд: git status --ignored покажет и неотслеживаемые, и игнорируемые файлы; если среди них есть то, без чего тесты не работают, — это будущее падение CI.
Вечнозелёный workflow. Самая опасная из всех, потому что выглядит как успех. Варианты:
- run: npm test || true # ошибку проглотили руками
- run: npm run lint
continue-on-error: true # шаг падает, job остаётся зелёным
- run: echo "тесты будут потом" # тестов нет вообще
Все три дают зелёную галочку при полностью сломанном проекте. || true превращает любой код возврата в ноль, continue-on-error — штатный ключ Actions, который иногда нужен, но в шаге с тестами превращает CI в декорацию. Проверка честности CI занимает одну минуту: сломай тест намеренно (поменяй ожидаемое значение), запушь, убедись, что стало красным, откати. CI, который ни разу не был красным, ещё не доказал, что умеет краснеть.
@main вместо версии у чужих action-ов. uses: some/action@main тянет последнюю версию чужого репозитория. В день, когда автор что-то поменяет, твой CI сломается сам по себе, без единого твоего коммита, — и полчаса уйдёт на поиск причины в собственном коде, которого никто не трогал.
Заводим CI на своём репозитории: пошагово
Сценарий целиком, от пустого репозитория до зелёной галочки. Пятнадцать минут в первый раз, три — во второй.
- Убедись, что команда работает локально в чистом клоне. Клонируй репозиторий в новую папку, выполни
npm ciиnpm test. Если здесь падает — CI тоже упадёт, и разбираться удобнее локально. - Создай файл.
.github/workflows/ci.yml— путь ровно такой. Содержимое — минимальный workflow из раздела выше. - Закоммить и запушь.
git add .github/workflows/ci.yml,git commit -m "добавил CI",git push. Workflow начинает работать с этого коммита: сам себя и запустит. - Открой вкладку Actions. Запуск появится в списке за несколько секунд. Первый прогон обычно дольше остальных: кэша ещё нет.
- Разбери результат. Зелёный — отлично. Красный — по алгоритму из раздела про логи: раскрыть упавший шаг, найти первую ошибку.
- Проверь, что CI умеет краснеть. Сломай один тест намеренно, запушь в отдельной ветке, убедись, что запуск красный, откати. Это единственный способ отличить работающий CI от декоративного.
- Повесь бейдж в README — чтобы состояние было видно, не открывая вкладку Actions.
Практика: заведи workflow руками
Робот включается не кнопкой, а файлом в нужном месте. Проверь, что помнишь
это место наизусть: перед тобой репозиторий без CI. Создай каталог
.github/workflows и положи внутрь deploy.yml — дальше GitHub подхватит
его сам.
- ○ .github/workflows/deploy.yml
Как это устроено под капотом
Вложенный каталог одной командой создаёт mkdir -p .github/workflows — без -p система откажется делать workflows внутри несуществующего .github. Пустой файл появляется через touch, а ls -a показывает каталоги, начинающиеся с точки.
Теперь набери вслепую строки, из которых состоит настоящий workflow, и команды, которыми смотрят его результат из терминала. В YAML отступ — часть синтаксиса, а не оформление: два пробела там, где они стоят в образце.
Цель: точность ≥ 92%
name: deploy
На чемпионате
CI на соревновании — инструмент с обеих сторон: он и приносит баллы, и съедает время, если обращаться с ним неаккуратно.
Где даёт баллы и время:
- Критерий «работоспособность сборки». Многие задания оценивают, собирается ли проект у эксперта. Зелёный CI — доказательство, что собирается на чистой машине, а не только на твоём ноутбуке. Это ровно тот балл, который теряют на фразе «у меня локально работает».
- Проверка перед сдачей за полминуты. В конце модуля нервы, времени нет, руками прогонять всё уже некогда. Пуш → взгляд на галочку — и ты знаешь, что сданное состояние действительно рабочее.
- Ловля незакоммиченного. Самая частая катастрофа на сдаче — файл, который есть у тебя и отсутствует в репозитории. CI ловит это на первом же пуше, а не на защите.
- Командные задания. Когда над репозиторием работают двое, CI — единственный нейтральный арбитр: чей коммит сломал сборку, видно по истории запусков, а не по спору.
Где съедает время и баллы:
- Настройка CI посреди модуля. Если workflow не готов заранее, писать его в отведённые часы — прямой убыток: это время не оценивается вообще. Заготовка
ci.ymlпод свой стек должна лежать в шпаргалке заранее, чтобы вставлялась копированием. - Починка CI вместо починки кода. Красный CI из-за опечатки в yml не влияет на оценку кода никак. Если workflow сломался, а времени в обрез, правильное решение — отложить его и доделать задание; вернуться, когда останется запас.
- Тяжёлая матрица. Матрица из шести комбинаций на большом проекте — это минуты ожидания после каждого пуша. На соревновании нужна скорость обратной связи, а не полнота покрытия: одна версия, один раннер.
- Ложная уверенность. Зелёный CI, который проверяет только сборку без тестов, ничего не гарантирует по функциональности. Знать, что именно проверяет твой workflow, — часть навыка.
Практический вывод: приходить на соревнование с готовым минимальным ci.yml под свой стек и вставлять его в первые пять минут работы над репозиторием, пока голова свежая, а не в последние.
Шпаргалка
| Ключ / шаг | Что делает |
|---|---|
.github/workflows/*.yml | единственное место, где GitHub ищет workflow |
name: | имя workflow в интерфейсе |
on: push / pull_request / workflow_dispatch | когда запускать: пуш / PR / кнопка ручного запуска |
jobs: | список задач; каждая получает свою чистую машину |
runs-on: ubuntu-latest | какую машину поднять (есть windows-latest, macos-latest) |
steps: | шаги по порядку; упал один — остальные пропущены |
uses: владелец/репо@версия | подключить готовый чужой action |
run: команда | выполнить свою команду в shell раннера |
with: | параметры конкретного action |
actions/checkout@v4 | привезти код репозитория на раннер (первым шагом) |
actions/setup-node@v4 | поставить версию Node; cache: npm включает кэш зависимостей |
actions/setup-java@v4 | то же для JDK: distribution: temurin, cache: gradle |
npm ci | установка строго из package-lock.json — воспроизводимо |
strategy.matrix | развернуть job в несколько параллельных (версии, ОС) |
fail-fast: false | не отменять остальные комбинации матрицы при первом падении |
secrets.ИМЯ / vars.ИМЯ | секрет из настроек репозитория / несекретная переменная |
actions/upload-artifact@v4 | сохранить папку как артефакт запуска |
actions/download-artifact@v4 | забрать артефакт в другом job-е |
needs: другой-job | выполнять только после успеха указанного job-а |
badge.svg в README | картинка со статусом последнего запуска |
Углубиться
CI и CD — разные буквы. CI (continuous integration) — автоматическая проверка каждого изменения. CD (continuous delivery/deployment) — автоматическая выкладка проверенного изменения на сервер. В GitHub Actions они пишутся одинаково и часто живут в одном файле, но разница принципиальна: CI можно включать смело в любой момент, CD — только когда есть уверенность, что проверки честные. Автоматическая выкладка нечестно-зелёной сборки хуже, чем отсутствие автоматизации вовсе.
Лимиты бесплатного тарифа. Публичным репозиториям минуты раннера не стоят ничего. Приватным выдаётся месячная квота, и Linux расходует её по курсу 1:1, Windows — вдвое быстрее, macOS — вдесятеро. Отсюда практика брать ubuntu-latest всюду, где нет прямой необходимости в другой системе.
permissions — про минимальные права. Каждому workflow выдаётся автоматический токен GITHUB_TOKEN для работы с репозиторием. По умолчанию прав у него больше, чем нужно проверке, поэтому в хороших workflow сверху пишут permissions: contents: read — «этому процессу можно только читать». Если чужой action окажется скомпрометирован, он не сможет ничего записать.
if: — условные шаги. У шага и у job-а есть ключ if, который включает их по условию: например, выкладывать только при пуше в main, а не на каждой ветке. Внутри — тот же язык выражений, что и в матрице.
Actions — не только GitHub. GitLab CI, Jenkins, TeamCity, Drone решают ту же задачу другим синтаксисом. Понятия совпадают почти один в один: триггер, job, шаг, артефакт, секрет. Выучив Actions, читать .gitlab-ci.yml можно почти без обучения — меняются слова, а не устройство.
Дальше по теме:
- GitHub Actions — официальная документация — первоисточник целиком
- Синтаксис workflow — полный список ключей с примерами
Сводная проверка по всей главе: 12 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Объяснять, зачем нужен CI, и называть минимум три расхождения между локальной машиной и чистым раннером.
- Создавать
.github/workflows/ci.ymlс триггерамиpush,pull_requestиworkflow_dispatchи понимать каждый ключ в нём. - Отличать
usesотrunи знать, почему у чужого action пинят версию, а не берут@main. - Объяснять, зачем нужен
actions/checkout, и узнавать его отсутствие по ошибке в логе. - Настраивать окружение через
setup-node(илиsetup-java) с фиксированной версией и кэшем зависимостей. - Объяснять разницу между
npm ciиnpm installи почему в CI используют первую. - Разворачивать матрицу версий и понимать цену её расширения.
- Хранить ключи в секретах репозитория, знать про маскирование в логах и про недоступность секретов форкам.
- Передавать результат сборки между job-ами через
upload-artifact/download-artifactиneeds. - Читать красный лог: найти упавший шаг, найти первую ошибку, отличить падение теста от падения окружения.
- Узнавать вечнозелёный workflow и проверять честность CI намеренно сломанным тестом.
Куда дальше: проверенные ресурсы
-
GitHub Actions — официальная документация — первоисточник: устройство, примеры, лимиты, безопасность.
-
Синтаксис workflow — полный справочник ключей: on, jobs, steps, strategy, permissions, if.
-
Использование секретов в GitHub Actions — как хранить ключи, как работает маскирование и почему форкам секреты не отдаются.
-
actions/checkout — README самого частого шага: все параметры, включая глубину клона.
-
actions/setup-node — версии Node, кэш зависимостей,
node-version-file. -
npm ci — документация npm — точное поведение команды и её отличия от
npm install. -
Бейдж статуса workflow — как собрать адрес картинки и привязать её к нужной ветке.
-
Спецификация YAML 1.2 — первоисточник по языку: отступы, списки, отображения и запрет табуляции.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Раннер, на котором крутится твой job, — обычная виртуальная машина с образом, который GitHub публикует открыто: список предустановленного софта, от компиляторов до браузеров, лежит в репозитории actions/runner-images. Именно поэтому Chrome для тестов ставить не нужно — он там уже есть. И именно поэтому образ иногда обновляется и что-то в нём меняется: фиксация версий рантайма через setup-* защищает как раз от этого.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.