Learn (Almost) Anything
КаталогСкачать курс

Автоматизация в GitHub

Как проектировать граф jobs: `needs`, `if`, status functions, outputs, `continue-on-error`, `timeout-minutes`, параллельность по умолчанию и когда нужен последовательный pipeline.

ruЭнциклопедия33 урока6 модулей9
GitHub actionsАвтоматизация в GitHubМодель workflowсостояние и артефактыматрицы и runtimeрелизы и environmentsПереиспользование и собственные actionsgovernance и эксплуатацияgithubactions

Jobs, dependencies и conditions

Как проектировать граф jobs: `needs`, `if`, status functions, outputs, `continue-on-error`, `timeout-minutes`, параллельность по умолчанию и когда нужен последовательный pipeline.

Jobs, dependencies и conditions

Job в GitHub Actions — это единица выполнения на runner: отдельная машина или контейнерный runtime, свой набор steps, свои permissions, timeout, outputs и статус. Внутри одного workflow jobs по умолчанию не образуют последовательный pipeline. Если между ними не указана зависимость через needs, GitHub старается запускать их параллельно, насколько позволяют доступные runners и лимиты.

Минимальная ментальная модель такая: on решает, создавать ли workflow run; jobs описывает граф работ; needs задает ребра графа; if решает, должен ли конкретный job или step выполняться в уже созданном run. Это важное различие: filters в Events и filters могут вообще не создать run, а if создает run, но помечает часть графа как skipped.

name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

В этом примере lint и test стартуют независимо. Это хорошая форма для CI: чем меньше искусственных зависимостей, тем быстрее обратная связь. Последовательность нужна не «для порядка», а когда один job реально потребляет результат другого, должен ждать его статуса или обязан идти после gate.

flowchart TD A[lint] --> D[ci-ok] B[test] --> D[ci-ok] C[typecheck] --> D[ci-ok] D --> E{ref == main?} E -->|yes| F[deploy] E -->|no| G[skip deploy] B --> H[upload failure logs] H -. if: failure() .-> D
flowchart TD
  A[lint] --> D[ci-ok]
  B[test] --> D[ci-ok]
  C[typecheck] --> D[ci-ok]
  D --> E{ref == main?}
  E -->|yes| F[deploy]
  E -->|no| G[skip deploy]
  B --> H[upload failure logs]
  H -. if: failure() .-> D
Граф jobs: независимые проверки идут параллельно, общий gate ждет их через `needs`, а deploy включается condition’ом.

needs: явный граф, а не скрытый порядок

needs говорит: «этот job можно запускать только после успешного завершения указанных jobs». Значение может быть строкой или массивом.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

Если lint или test упадет, build будет skipped. Если skipped или failure случились в начале цепочки, downstream jobs тоже обычно skipped. Поэтому длинный линейный pipeline стоит строить только там, где он отражает реальную зависимость: например, buildpublish imagedeploy production.

Плохой запах — pipeline, где lint ждет test, test ждет build, хотя они проверяют один и тот же checkout. Такой граф медленнее и менее информативен: первая ошибка скрывает остальные. Лучше запускать независимые проверки параллельно, а затем собирать общий gate.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - run: npm run typecheck

  ci-ok:
    needs: [lint, test, typecheck]
    runs-on: ubuntu-latest
    steps:
      - run: echo "CI gate passed"

Такой ci-ok часто используют как один required check в branch protection. Но есть нюанс: если upstream job skipped из-за if, gate тоже может стать skipped. Для сложных required checks это уже тема Monitoring и troubleshooting.

if: condition на job и step

if можно ставить на job и на step. На job-уровне он решает, попадет ли весь job в исполнение; на step-уровне — выполнится ли конкретная команда или action.

jobs:
  deploy:
    if: github.ref == 'refs/heads/main'
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/deploy.sh

В if на job GitHub автоматически воспринимает выражение как expression, поэтому часто пишут без ${{ }}. Но если выражение начинается с !, лучше явно обернуть его: YAML использует ! как специальную нотацию.

if: ${{ ! startsWith(github.ref, 'refs/tags/') }}

Важно: jobs.<job_id>.if вычисляется до применения strategy.matrix. Поэтому нельзя на этом уровне обращаться к matrix.node так, будто matrix уже развернута. Условия, зависящие от конкретной комбинации matrix, обычно ставят внутри steps или выражают через саму matrix-конфигурацию. Подробная механика matrix вынесена в Matrix strategy, а синтаксис contexts и функций — в Contexts и expressions.

Status functions: success, failure, cancelled, always

Условия в Actions имеют скрытый default: если в if нет status function, GitHub фактически применяет success(). Поэтому step с if: steps.build.outcome == 'failure' может не сработать после ошибки, если не добавить failure().

steps:
  - name: Build
    id: build
    run: npm run build

  - name: Upload logs after build failure
    if: ${{ failure() && steps.build.conclusion == 'failure' }}
    uses: actions/upload-artifact@v4
    with:
      name: build-logs
      path: logs/

success() возвращает true, когда предыдущие steps успешны. failure() полезен для диагностики после падения; в цепочке зависимых jobs он также учитывает failure предков. cancelled() нужен для отдельных cleanup-сценариев. always() запускает step или job даже после failure/cancel, но его не стоит ставить на критичные операции вроде checkout или подготовки окружения: можно получить job, который висит до timeout. Для «сделать, если workflow не был отменен» часто безопаснее условие if: ${{ !cancelled() }}.

Job outputs: данные между jobs

Jobs изолированы. Переменная shell из build не появится в deploy. Чтобы передать небольшое структурное значение, используют step output → job output → needs.<job_id>.outputs.

jobs:
  plan:
    runs-on: ubuntu-latest
    outputs:
      deploy_target: ${{ steps.pick.outputs.target }}
    steps:
      - id: pick
        run: |
          if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
            echo "target=production" >> "$GITHUB_OUTPUT"
          else
            echo "target=staging" >> "$GITHUB_OUTPUT"
          fi

  deploy:
    needs: plan
    if: needs.plan.outputs.deploy_target == 'production'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ needs.plan.outputs.deploy_target }}"

Outputs подходят для коротких строк, JSON-фрагментов и решений графа. Для файлов, отчетов, coverage и build outputs лучше использовать Artifacts и reports. Для зависимостей — Dependency cache, не outputs.

continue-on-error и timeout

continue-on-error меняет смысл падения. На step-уровне он позволяет job продолжить выполнение после неуспешного step. На job-уровне он может позволить workflow run считаться успешным, даже если этот job failed. Это удобно для experimental checks, nightly compatibility или non-blocking security audit, но опасно для обязательного CI: легко случайно сделать красный сигнал невидимым.

jobs:
  test-experimental:
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - run: npm test -- --experimental-runtime

В matrix это обычно делают точечно: stable-комбинации блокируют pipeline, experimental-комбинации дают сигнал, но не ломают весь run. С этим рядом живут strategy.fail-fast и strategy.max-parallel: они отвечают не за зависимости, а за поведение группы matrix jobs.

timeout-minutes задает верхнюю границу выполнения. У job default — 360 минут; у step тоже можно ставить timeout, максимум 360 минут. Практический смысл timeout — не только экономия минут. Он делает зависание явным failure, особенно для e2e-тестов, Docker build и deploy-команд.

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - run: npm run test:e2e
        timeout-minutes: 15

Когда pipeline должен быть последовательным

Последовательный граф оправдан, когда есть материальный артефакт или gate: сначала собрать контейнер, потом просканировать image, потом задеплоить; сначала создать release, потом опубликовать package; сначала пройти environment approval, потом получить environment secrets. В Deployment workflows это становится особенно важно: деплой — не просто еще один test job, а изменение внешней системы.

Для CI чаще работает другой принцип: максимум независимых проверок параллельно, один небольшой aggregation job в конце, минимум side effects. Тогда workflow быстрее, failures легче читать, а граф в Actions UI действительно показывает архитектуру проверки, а не случайный порядок строк в YAML.

ИнтерактивМини-справочник по условиям jobs#int-1
1 / 6
Короткие карточки по ключам, которые управляют графом jobs и поведением после ошибок.

See also

Быстрое повторение #rc-1

В workflow есть два job: lint и test, и между ними нет needs. Как GitHub Actions будет их запускать?

Показать ответ

Они независимы, поэтому GitHub будет стараться запускать их параллельно, насколько позволяют runners и лимиты. Порядок в YAML сам по себе не делает pipeline последовательным.

Источники

  1. GitHub Docs — Workflow syntax for GitHub Actions
  2. GitHub Docs — Evaluate expressions in workflows and actions
  3. GitHub Docs — Passing information between jobs