Загрузка...

Баг в пайплайне: почему постоянный PR dev → main удваивает запуски GitHub Actions и как это исправить

Баг в пайплайне: почему постоянный PR dev → main удваивает запуски GitHub Actions и как это исправить

При использовании интеграционной ветки (GitFlow или схема с постоянным открытым PR dev → main) стандартный триггер в GitHub Actions приводит к скрытому оверхеду: каждый пуш в dev запускает не один пайплайн, а два идентичных прогона одновременно. В итоге удваивается время сборки контейнеров, повторно прогоняются тесты и в два раза быстрее выжигается лимит бесплатных минут раннеров. Проблема кроется в механике обработки событий push и pull_request.synchronize.

Анатомия проблемы: почему GitHub запускает два воркфлоу

Типовой блок триггеров в .github/workflows/ci.yml часто копируют из документации без учета постоянных релизных PR:

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]

Замысел триггера понятен: гонять тесты на прямых коммитах в main и dev, а также валидировать входящие Pull Request. Но если в репозитории постоянно открыт релизный PR из dev в main, команда git push origin dev инициирует два независимых события:

  1. Событие push: GitHub фиксирует обновление ветки dev и запускает воркфлоу по первому правилу.
  2. Событие pull_request (действие synchronize): GitHub проверяет открытые PR, видит обновление ветки-источника (dev) для PR в main и по второму правилу запускает второй параллельный воркфлоу на тот же самый коммит.

Почему стандартный concurrency не отменяет дубли

Попытка изолировать параллельные запуски через базовый блок concurrency не дает результата:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Причина — в формировании контекстной переменной github.ref для разных типов событий:

  • Для события push значение github.ref равно refs/heads/dev.
  • Для события pull_request значение github.ref равно refs/pull/<pr_number>/merge.

Планировщик GitHub Actions считает ci-refs/heads/dev и ci-refs/pull/105/merge двумя разными группами параллелизма. Они не отменяют друг друга, и оба воркфлоу выполняются параллельно на раннерах.

Решение: фильтрация на уровне джоб и нормализация группы

Для полного устранения дублирования вносятся две точечные правки в конфигурацию воркфлоу.

1. Условие пропуска повторных проверок (skip redundant PR checks)

На уровне каждой джобы (линтеры, анализ типов, автотесты) задается проверка источника PR:

jobs:
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || github.head_ref != 'dev'
    steps:
      - uses: actions/checkout@v4
      # ...

  tests:
    name: Tests & Coverage
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || github.head_ref != 'dev'
    steps:
      - uses: actions/checkout@v4
      # ...

Логика работы условия:

  • При событии push первая часть условия (github.event_name != 'pull_request') возвращает true — джоба штатно запускается.
  • При событии pull_request проверяется вторая часть: github.head_ref != 'dev'.
  • Если это PR из dev в main, условие возвращает false, и джоба пропускается (так как эти коммиты уже проверяются в соседнем событии push).
  • Если открыт обычный фиче-ветковый PR (например, feature/billing → dev), условие возвращает true, и тесты выполняются в полном объеме.

2. Унификация группы concurrency по имени ветки

Вместо разнородного github.ref группа привязывается к фактическому имени ветки через оператор ||:

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: true

Механика подстановки:

  • В контексте pull_request переменная github.head_ref содержит имя ветки-источника (feature/billing или dev).
  • В контексте push переменная github.head_ref пуста, и выражение откатывается на github.ref_name (краткое имя ветки, например dev).

В результате все события вокруг одной ветки получают одинаковый строковый ключ. А параметр cancel-in-progress: true автоматически отменяет предыдущий незавершенный прогон при быстром повторном пуше, не занимая очередь сборки.

Итоговый шаблон ci.yml

Рабочая конфигурация пайплайна с защитой от дублирующих прогонов:

name: CI

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: true

jobs:
  tests:
    name: Tests (PHP ${{ matrix.php }})
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || github.head_ref != 'dev'
    strategy:
      matrix:
        php: ['8.3', '8.4']
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
      - run: composer install --prefer-dist --no-interaction
      - run: php artisan test

Результат

  • Полное устранение задвоенных воркфлоу в списке Actions.
  • Двукратная экономия доступных минут на раннерах.
  • Автоматическая отмена устаревших запусков при серии последовательных пушей.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *