Перейти к основному содержимому
Отдельные темы05ГЛАВА 05CI: робот проверяет за тебяon: push

CI: робот проверяет за тебя

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/2
Уровень 1 · Новичок

Зачем это нужно​

Самая дорогая фраза в разработке — «у меня локально работает». Она произносится ровно в тот момент, когда работа уже сдана, а у проверяющего она не собирается: не хватает файла, который забыли добавить в git; проект собирается только на твоей версии Node; тесты проходят, потому что в твоей папке лежит config.local.json, которого нет ни у кого больше. Локальная машина — это не «чистая» среда, а место, где за месяцы работы накопилось столько всего, что уже непонятно, чем именно ты держишься на плаву.

CI (continuous integration, непрерывная интеграция) решает эту проблему одним движением: каждый раз, когда ты отправляешь коммит, сервер поднимает чистую машину, забирает на неё твой репозиторий ровно в том виде, в каком он лежит в git, и прогоняет то, что ты попросишь — сборку, тесты, линтер. Одинаково. Всегда. Без «ой, у меня же ещё вчерашняя ветка была открыта».

Эта глава — про GitHub Actions: самый распространённый CI, встроенный прямо в GitHub, где ничего не нужно поднимать и настраивать снаружи. Дальше предполагается, что коммит, ветка, push и pull request — уже знакомые слова.

Что робот проверяет такого, чего не проверишь сам​

Формально ничего сверхъестественного: те же самые команды, что ты набираешь в терминале. Разница — в среде, и именно она ловит ошибки.

  • Незакоммиченный файл. Тесты на твоей машине читают 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 · ЧТО ЗА ЧТО ОТВЕЧАЕТ.github/workflows/ci.ymlname: cion: [push]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm testonкогда запускатьjobsчто запускатьruns-onна какой машинеusesготовый чужой шагrunсвоя команда в shellфайл лежит только в .github/workflows — в другой папке GitHub его просто не увидит
Файл workflow лежит только в .github/workflows: on задаёт когда, jobs — что, runs-on — на какой машине, uses и run — чем именно занят каждый шаг.

Минимальный 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».

ACTIONS/CHECKOUT · КОД НА РАННЕР НЕ ПОПАДАЕТ САМбез checkoutрабочая папка раннера: пусто(нет файлов)npm ciENOENT: no such file package.jsonпосле actions/checkout@v4package.jsonsrc/ · tests/npm ciadded 412 packagesраннер — чистая машина: без шага checkout репозиторий на неё не приезжает
Раннер стартует пустым: без шага actions/checkout первая же команда упирается в отсутствующий package.json, с ним — работает как дома.

Порядок тоже важен: 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 installnpm 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 и заводят: рассинхрон, который дома не виден вообще.

Отвечено вопросов: 0 из 5
В какой папке репозитория GitHub ищет файлы workflow?
Чем uses отличается от run в шаге workflow?
Почему шаг run: npm test падает с ENOENT, если перед ним нет actions/checkout?
Почему в CI используют npm ci, а не npm install?
Что делает workflow_dispatch в списке триггеров?
Отвечено верно: 0 из 5

Матрица версий​

Иногда проверить нужно не на одной версии, а на нескольких: «работает ли проект и на 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 — job deploy ждёт успешного завершения job-а build. Без needs job-ы стартуют одновременно, и deploy побежит скачивать артефакт, которого ещё нет. needs — единственный способ задать порядок между job-ами.
  • download-artifact распаковывает артефакт на новую машину. Обрати внимание: папку build во втором job-е не «видно из первого» — она физически появляется там только благодаря этой паре шагов.
NEEDS И АРТЕФАКТЫ · ДВА JOB-А, РАЗНЫЕ МАШИНЫjob: buildсвоя чистая машинаnpm ci · npm testnpm run build → build/upload-artifactартефактsite.zipjob: deployneeds: builddownload-artifactrsync на сервертоже чистая машинабез needs job-ы стартуют одновременно; папка build/ из одного job-а во втором не существуетединственный мост между ними — артефакт
Два job-а — две разные чистые машины. needs задаёт порядок, артефакт — единственный мост, по которому папка build переезжает из сборки в выкладку.

Когда стоит разбивать на job-ы вместо одного длинного списка шагов: когда части независимы (линтер и тесты могут идти параллельно), когда нужны разные машины (сборка на Linux, проверка на Windows) или когда вторая часть должна выполняться не всегда. Если всё делается подряд на одной машине — не усложняй: один job, много шагов, никаких артефактов.

Бейдж статуса в README​

Результат последнего запуска можно вывести картинкой прямо в README — это первое, что видит человек, открывший репозиторий:

![ci](https://github.com/ВЛАДЕЛЕЦ/РЕПОЗИТОРИЙ/actions/workflows/ci.yml/badge.svg)

Адрес собирается механически: владелец, репозиторий, дальше /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.

Полезного здесь ровно четыре строки в начале: какой файл, какой тест, что ожидалось и что получилось, в каком месте кода. Остальные пять — это эхо, которое каждый слой печатает поверх предыдущего.

КРАСНЫЙ ЛОГ · ЧИТАТЬ СВЕРХУ ВНИЗSet up job✓actions/checkout@v4✓actions/setup-node@v4✓npm ci✓npm test✕npm run build—шаги после упавшего не выполняютсяFAIL tests/cart.test.jsexpected 200, received 500at total (src/cart.js:42)Tests: 1 failed, 23 passednpm ERR! code ELIFECYCLEError: Process exited with 1↑ последние строки — только эхопричина — в первой красной строке
Упавший шаг ровно один, за ним всё пропущено. Причина — в первых строках его вывода; хвост про 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. Один такой пуш даёт больше, чем полчаса рассуждений о том, как оно там устроено.

Отвечено вопросов: 0 из 4
В конце лога написано Error: Process completed with exit code 1. О чём говорит эта строка?
В логе видно: Cannot find module "./utils". Тесты локально проходят. Самая вероятная причина?
Workflow упал с You have an error in your yaml syntax on line 7. Что сломано?
После упавшего шага следующие шаги показаны серыми. Почему?
Отвечено верно: 0 из 4

Типичные ошибки​

Шесть граблей, на которые наступают почти все — лучше здесь, чем на сдаче.

Табы в 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 сломается сам по себе, без единого твоего коммита, — и полчаса уйдёт на поиск причины в собственном коде, которого никто не трогал.

Отвечено вопросов: 0 из 5
Зачем нужен ключ needs: build у второго job-а?
Почему нельзя записать SSH-ключ прямо в файл workflow?
Pull request пришёл из чужого форка. Что произойдёт с шагом, которому нужен secrets.DEPLOY_KEY?
Что даёт fail-fast: false в strategy.matrix?
Шаг с тестами написан как run: npm test || true. Чем это плохо?
Отвечено верно: 0 из 5

Заводим CI на своём репозитории: пошагово​

Сценарий целиком, от пустого репозитория до зелёной галочки. Пятнадцать минут в первый раз, три — во второй.

  1. Убедись, что команда работает локально в чистом клоне. Клонируй репозиторий в новую папку, выполни npm ci и npm test. Если здесь падает — CI тоже упадёт, и разбираться удобнее локально.
  2. Создай файл. .github/workflows/ci.yml — путь ровно такой. Содержимое — минимальный workflow из раздела выше.
  3. Закоммить и запушь. git add .github/workflows/ci.yml, git commit -m "добавил CI", git push. Workflow начинает работать с этого коммита: сам себя и запустит.
  4. Открой вкладку Actions. Запуск появится в списке за несколько секунд. Первый прогон обычно дольше остальных: кэша ещё нет.
  5. Разбери результат. Зелёный — отлично. Красный — по алгоритму из раздела про логи: раскрыть упавший шаг, найти первую ошибку.
  6. Проверь, что CI умеет краснеть. Сломай один тест намеренно, запушь в отдельной ветке, убедись, что запуск красный, откати. Это единственный способ отличить работающий CI от декоративного.
  7. Повесь бейдж в README — чтобы состояние было видно, не открывая вкладку Actions.
Отвечено вопросов: 0 из 3
Ты добавил .github/workflows/ci.yml и запушил в ветку feature/cart. В on указан только push с branches: [main]. Что будет во вкладке Actions?
Как убедиться, что твой CI действительно проверяет код, а не просто светит зелёным?
Почему первый запуск workflow обычно дольше следующих?
Отвечено верно: 0 из 3

Практика: заведи workflow руками​

Робот включается не кнопкой, а файлом в нужном месте. Проверь, что помнишь это место наизусть: перед тобой репозиторий без CI. Создай каталог .github/workflows и положи внутрь deploy.yml — дальше GitHub подхватит его сам.

Задание: Создай .github/workflows/deploy.yml (каталога .github ещё нет)
  • ○ .github/workflows/deploy.yml
Учебный терминал. Введите help, чтобы увидеть список команд.
student@pgk:~$
Как это устроено под капотом
Никакого настоящего Linux здесь нет: файловая система — обычный JS-объект в памяти браузера, где папка — это вложенный объект, а файл — строка с содержимым. Команды ls, cd, mkdir, rm и остальные — функции, которые читают и меняют этот объект, а cp -r — просто глубокое копирование куска JSON. После каждой команды проверка квеста проходит по требуемым путям и смотрит, существуют ли они в этом дереве. Перезагрузишь страницу — «диск» создастся заново с нуля, так что ломать тут можно безнаказанно.
Совет

Вложенный каталог одной командой создаёт 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 можно почти без обучения — меняются слова, а не устройство.

Дальше по теме:

Сводная проверка по всей главе: 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 намеренно сломанным тестом.

Куда дальше: проверенные ресурсы​

Интересный факт

Раннер, на котором крутится твой job, — обычная виртуальная машина с образом, который GitHub публикует открыто: список предустановленного софта, от компиляторов до браузеров, лежит в репозитории actions/runner-images. Именно поэтому Chrome для тестов ставить не нужно — он там уже есть. И именно поэтому образ иногда обновляется и что-то в нём меняется: фиксация версий рантайма через setup-* защищает как раз от этого.

Видео по теме

ВидеоGithub Actions - Введение в CI/CDВладилен Минин
ВидеоПро GitHub Actions за 10 минутMerion Academy
ВидеоGitHub Actions для CI/CD - mini курс за 30 минутPurpleSchool | Anton Larichev
ВидеоGitHub Actions для автоматической проверки кодаХитрый питон
ВидеоGitHub Actions - Основы Автоматизации - DevOps - GitOpsADV-IT

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.