open atlas
↑ К треку
CI/CD-пайплайны CICD · 01 · 01

GitHub Actions: workflows, jobs, steps

Workflow — это YAML-файл в .github/workflows, запускаемый событиями on:. Он содержит jobs, которые по умолчанию идут параллельно на свежих runner-VM; каждый job состоит из steps, где шаг либо использует action, либо запускает shell-команду.

CICD Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Разработчик открывает pull request и смотрит на вкладку Actions. Тесты прошли. Он мёржит — и job деплоя, который собрал свою копию приложения на другом runner, падает, потому что ожидаемого артефакта никогда не было: job тестов собрал его в своей VM, и эту VM выбросили в тот момент, когда job завершился. Ничего не передалось. Модель в этом беспощадна: каждый job — это чистая машина, которая не знает о других ничего, пока ты сам их не свяжешь. Выучи четыре существительных — workflow, job, step, runner — и большая часть путаницы в CI испаряется.

Четыре существительных: workflow, job, step, runner

Всё в GitHub Actions — это одна из четырёх вещей. Workflow — это один YAML-файл, лежащий в .github/workflows/ твоего репозитория: ci.yml, deploy.yml, по файлу на workflow. Workflow содержит один или несколько jobs. Каждый job выполняется на runner — виртуальной машине (GitHub-hosted ubuntu-latest или твой self-hosted сервер), которая поднимается с нуля под этот job и уничтожается, когда он заканчивается. Внутри job — упорядоченный список steps, и каждый step делает ровно одно из двух: либо uses: опубликованный action, либо run: shell-команду.

Триггер стоит наверху. Ключ on: объявляет, какие события запускают workflow. Четыре, к которым тянешься постоянно: push (коммит прилетел в ветку), pull_request (PR открыт или обновлён), workflow_dispatch (человек нажал «Run workflow» — кнопка ручного запуска) и schedule (cron-выражение, например ночной прогон). Один workflow может слушать сразу несколько событий.

name: CI
on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:        # кнопка ручного запуска
  schedule:
    - cron: '0 3 * * *'     # ночью в 03:00 UTC

jobs:
  test:
    runs-on: ubuntu-latest  # runner: свежая VM
    steps:
      - uses: actions/checkout@v4        # action: клонировать репозиторий
      - uses: actions/setup-node@v4      # action: поставить Node
        with:
          node-version: 22
          cache: npm
      - run: npm ci                      # shell-команда
      - run: npm test

Это полноценный, реальный CI-workflow: на каждый push в main и каждый PR подними свежую Ubuntu-VM, выкачай код, поставь Node 22, детерминированно установи зависимости через npm ci и запусти тесты. actions/checkout и actions/setup-node — канонические первые два шага почти любого Node-workflow: runner стартует пустым, поэтому ты обязан явно склонировать собственный репозиторий и поставить тулчейн, прежде чем что-либо ещё сможет выполниться.

Jobs по умолчанию параллельны и изолированы

Это правило удивляет. Перечисли два job в одном workflow — и они выполнятся одновременно, на отдельных runner’ах. GitHub не сериализует их за тебя. Это фича — твои lint, type-check и тесты завершатся за время самого медленного, а не их суммы, — но это значит, что ни один job не видит файловую систему, окружение или вывод другого. Каждый runner — это чистая VM, которая заканчивается в тот момент, когда заканчивается её job.

Чтобы навязать порядок, используй needs:. Job с needs: [test] не стартует, пока test не завершится успешно, превращая плоское множество jobs в направленный ациклический граф (DAG). Так ты выражаешь «lint и тесты параллельно, потом деплой — только если оба прошли».

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [{ uses: actions/checkout@v4 }, { run: npm run lint }]
  test:
    runs-on: ubuntu-latest
    steps: [{ uses: actions/checkout@v4 }, { run: npm test }]
  deploy:
    needs: [lint, test]      # ждёт оба; запускается только если оба прошли
    runs-on: ubuntu-latest
    steps: [{ run: ./deploy.sh }]

Поскольку каждый job — это собственная VM, ты не можешь просто передать файл из одного job в следующий. Если build создаёт папку dist/, нужную deploy, то job сборки должен выгрузить её через actions/upload-artifact, а job деплоя — скачать через actions/download-artifact. Аналогично, per-runner кэш (actions/cache или сокращение cache: у setup-node) сохраняет вещи вроде node_modules между прогонами во времени — это не канал для передачи данных между jobs в одном прогоне. Артефакты двигают данные вбок внутри прогона; кэш двигает данные вперёд между прогонами.

Почему это работает

Почему ${{ github.sha }} работает, а голое github.sha — нет? У GitHub Actions есть собственный язык выражений, вычисляемый до того, как runner что-либо выполнит. ${{ }} — это маркер, который говорит «вычисли это выражение и подставь результат». Внутри него ты читаешь контексты — структурированные объекты вроде github (метаданные события: github.sha, github.ref, github.actor), env, secrets, matrix и runner. Вне скобок github.sha — это просто буквальный текст. Поэтому run: echo ${{ github.sha }} печатает хэш коммита, а run: echo github.sha печатает слова. Контексты — это то, как workflow узнаёт, что его запустило, и подстраивается.

Поток одного прогона

Когда срабатывает событие, GitHub читает подходящий файл workflow, строит граф jobs из needs: и отправляет каждый готовый job на runner. Внутри каждого runner steps выполняются сверху вниз, и первый упавший шаг (ненулевой код выхода) роняет job, если ты не отказался от этого явно. Вот вся цепочка от начала до конца.

СуществительноеЧто этоАналогия
workflowОдин YAML-файл в .github/workflows/, запускаемый событиями on:Книга рецептов, открытая поводом (push, расписание)
jobЕдиница работы; по умолчанию параллельна, порядок — через needs:Одно блюдо — несколько готовятся разом, пока одно не ждёт другого
stepОдин action (uses:) или одна shell-команда (run:), по порядкуОдна инструкция в рецепте
runnerСвежая VM (runs-on:), на которой идёт job, уничтожаемая послеОдноразовая кухня, отдраенная и выброшенная под каждое блюдо
Почему это работает

Сеньорская гигиена, превращающая рабочий workflow в безопасный. Пинни сторонние actions к полному commit SHA, а не к подвижному тегу: uses: some/action@a1b2c3d… неизменяем, тогда как @v3 может быть перенацелен тем, кто владеет тегом — дыра в цепочке поставок. Ставь least-privilege permissions:, в идеале по умолчанию read-only, и поднимай права по конкретному job только там, где нужно, чтобы скомпрометированный шаг не смог запушить в твой репозиторий через авто-выданный GITHUB_TOKEN. Добавь concurrency: с cancel-in-progress, чтобы новый push отменял уже устаревший прогон на той же ветке, а не жёг runner на превзойдённом коде.

Викторина

В workflow есть job сборки и job деплоя. deploy падает, потому что папки dist/, созданной build, нет. Почему?

Выбери лучший вариант

Нужен шаг, который клонирует твой репозиторий на свежий runner, прежде чем тесты смогут прочитать код. Выбери правильный механизм.

Вспомните перед уходом
  1. 01
    Дай определение workflow, job, step и runner и объясни, как один прогон проходит через все четыре.
  2. 02
    Почему jobs идут параллельно, что меняет needs: и почему нельзя просто делить файлы между jobs?
Итог

GitHub Actions сводится к четырём существительным. Workflow — это один YAML-файл в .github/workflows/, запускаемый событиями, которые ты перечисляешь под on: — push, pull_request, ручной workflow_dispatch или schedule по cron. Workflow держит jobs, и jobs по умолчанию идут параллельно, каждый на своём runner: свежей виртуальной машине, названной в runs-on:, которая создаётся под job и уничтожается по его завершении. Job — это упорядоченный список steps, где каждый шаг либо uses: опубликованный action — actions/checkout и actions/setup-node канонически первые два, потому что runner стартует пустым, — либо run: shell-команду. Минимальный CI ровно таков: checkout, настрой тулчейн, поставь через npm ci, прогони тесты. Два следствия определяют модель. Первое: jobs изолированы — используй needs:, чтобы упорядочить их в DAG, и переноси данные артефактами (вбок внутри прогона) или кэшем (вперёд между прогонами), а не предполагая общий диск. Второе: workflow читает собственный контекст через синтаксис выражений ${{ github.sha }}, который движок вычисляет до выполнения шагов. Добавь сеньорскую гигиену — пинни actions к commit SHA, давай least-privilege permissions: и отменяй превзойдённые прогоны через concurrency: — и рабочий пайплайн становится безопасным. Теперь, когда в следующий раз увидишь deploy-job с ошибкой «нет такого файла», первый вопрос — не «почему упала сборка», а «передал ли build арtefакт через upload-artifact».

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.