CI/CD, инфраструктура и облака GitHub Actions, GitLab CI, Drone и другие: сравнение современных платформ CI
0%

GitHub Actions, GitLab CI, Drone и другие: сравнение современных платформ CI

GitHub Actions, GitLab CI, Drone и другие: сравнение современных платформ CI

В предыдущей статье трека — Jenkins — мы разобрали классическую модель: отдельный сервер, который сам себе и планировщик, и веб-интерфейс, и склад плагинов. Современные платформы CI построены иначе, и разница не косметическая. Она в том, кто владеет состоянием, где живёт описание пайплайна и кто платит за железо.

Эта статья — практическое сравнение. Мы соберём один и тот же реальный проект (Go-сервис с тестами, линтером, сборкой Docker-образа и публикацией в реестр) на трёх платформах, посмотрим на архитектурные различия, а потом честно посчитаем деньги: когда SaaS-минуты дешевле своих раннеров, а когда наоборот.


Откуда взялась нынешняя архитектура

Полезно понимать эволюцию — она объясняет, почему у GitLab CI есть stages, у GitHub Actions — uses:, а у Drone всё крутится вокруг Docker-образов.

Ключевой перелом — 2011 год и .travis.yml. До него конфигурация сборки была свойством сервера, после — свойством коммита. Это меняет всё: пайплайн ревьюится, откатывается, ветвится вместе с кодом. Ветка feature/x может собираться иначе, чем main, и это не требует ничьих прав администратора.

Второй перелом — отделение control plane от исполнителей. Сервер только хранит очередь заданий и логи; вычисления делают раннеры/агенты, которые могут быть где угодно.

Обратите внимание на направление стрелок к раннерам: они пунктирные и исходят от раннера. Раннер сам ходит за работой (long-polling), сервер не инициирует соединение. Практическое следствие: свой раннер можно поставить внутри закрытого контура без единого входящего порта — ему нужен только исходящий HTTPS. Это то, чего у классического Jenkins-мастера с JNLP-агентами добиться было ощутимо сложнее.


Общая модель: события, задания, DAG

Все современные платформы описываются одной абстракцией:

  • Триггер — событие (push, merge request, тег, расписание, ручной запуск, вызов из другого пайплайна).
  • Workflow / pipeline — набор заданий с зависимостями.
  • Job — единица планирования: выполняется целиком на одном раннере, в одной файловой системе.
  • Step — команда или переиспользуемый блок внутри задания.
  • Артефакты и кэш — способ передать данные между заданиями (через хранилище платформы, не через диск).

Различие в терминах, но не в сути:

Понятие GitHub Actions GitLab CI Drone / Woodpecker Tekton
Файл конфигурации .github/workflows/*.yml .gitlab-ci.yml .drone.yml / .woodpecker.yml CRD в кластере
Единица планирования job job step (в pipeline) Task
Шаг step (run или uses) элемент script контейнер step в Task
Группировка needs (явный DAG) stages + needs depends_on Pipeline
Переиспользование actions, reusable workflows include, CI components плагины-образы Task в каталоге
Исполнитель runner (VM/контейнер) runner (7 executors) контейнер по шагу под в Kubernetes

Жизненный цикл задания стоит держать в голове — именно на переходах между состояниями возникает большинство «непонятных» проблем.

Состояние Stuck (в GitLab оно так и называется) — классическая ошибка новичка: задание помечено тегом docker-arm64, а раннера с таким тегом никто не зарегистрировал. Пайплайн висит вечно и не падает. Всегда ставьте таймаут на задание.


GitHub Actions

Модель: пайплайн — набор workflow-файлов, каждый подписан на события репозитория. Главная особенность — экосистема переиспользуемых шагов (uses:), которая одновременно и сильнейшая сторона, и главный источник проблем с безопасностью.

Рабочий конфиг для нашего Go-сервиса:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
  workflow_dispatch:      # ручной запуск из UI

# Отменяем предыдущие запуски по той же ветке — экономия минут
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

permissions:
  contents: read          # принцип наименьших прав: по умолчанию всё read

env:
  GO_VERSION: "1.23"

jobs:
  lint:
    runs-on: ubuntu-24.04
    steps:
      # Пиннинг по SHA, а не по тегу: тег в чужом репозитории можно переписать
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed  # v5.1.0
        with:
          go-version: ${{ env.GO_VERSION }}
          cache: true     # кэш модулей и build cache «из коробки»
      - uses: golangci/golangci-lint-action@v6
        with:
          version: v1.62

  test:
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false    # не гасить всю матрицу из-за одной ячейки
      matrix:
        go: ["1.22", "1.23"]
        include:
          - go: "1.23"
            coverage: true
    services:
      # Сервисные контейнеры доступны по localhost — удобно для интеграционных тестов
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-retries 10
        ports: ["5432:5432"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "${{ matrix.go }}", cache: true }
      - name: Тесты с гонками
        run: go test -race -timeout 5m -coverprofile=cover.out ./...
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/postgres?sslmode=disable
      - name: Отчёт о покрытии
        if: matrix.coverage
        run: go tool cover -func=cover.out | tail -1

  image:
    needs: [lint, test]
    if: github.event_name == 'push'
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      packages: write     # публикация в ghcr.io
      id-token: write     # OIDC-токен для облака, без долгоживущих ключей
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # временный токен запуска
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          provenance: true                        # SLSA-аттестация сборки

Что здесь важно по существу:

  1. concurrency — самая недооценённая экономия. Разработчик пушит пять раз подряд, платформа честно запускает пять пайплайнов. Отмена предыдущих режет счёт на десятки процентов.
  2. Пиннинг по SHA. Тег @v4 в чужой action — это указатель, который владелец может передвинуть. Инцидент с tj-actions/changed-files в марте 2025 года — ровно этот сценарий: злоумышленник переписал теги, и action начала выгружать секреты из памяти раннера в логи (разбор StepSecurity). Пиннинг по SHA плюс permissions: read — минимальная гигиена.
  3. id-token: write и OIDC. Вместо того чтобы класть в секреты вечный AWS_SECRET_ACCESS_KEY, раннер получает короткоживущий токен и обменивает его на роль. Подробнее — в статье о безопасности конвейера.

Обмен OIDC-токена стоит представлять целиком:

Сильные стороны GitHub Actions: нулевой порог входа, огромный marketplace, бесплатность для публичных репозиториев, отличная интеграция с самим GitHub (статусы, окружения, деплой-гейты, GITHUB_TOKEN со скоупами).

Слабые: YAML-выражения ${{ }} — это отдельный мини-язык без нормальной отладки; матрицы плохо параметризуются динамически (приходится генерировать JSON в предыдущем задании и подавать через fromJSON); нет встроенного способа запустить пайплайн локально (act — эмуляция, не эквивалент); marketplace — это чужой код с полным доступом к раннеру.

Динамическая матрица — приём, который приходится знать:

  discover:
    runs-on: ubuntu-24.04
    outputs:
      services: ${{ steps.gen.outputs.list }}
    steps:
      - uses: actions/checkout@v4
      - id: gen
        # Собираем список изменённых сервисов и отдаём JSON-массив
        run: |
          list=$(ls services | jq -R -s -c 'split("\n")[:-1]')
          echo "list=$list" >> "$GITHUB_OUTPUT"

  build:
    needs: discover
    strategy:
      matrix:
        service: ${{ fromJSON(needs.discover.outputs.services) }}
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Собираю ${{ matrix.service }}"

GitLab CI/CD

Модель: один файл .gitlab-ci.yml, стадии по умолчанию последовательные, но с needs превращаются в DAG. Главное отличие от GitHub — раннеры это первоклассная сущность с семью исполнителями (shell, docker, docker-autoscaler, kubernetes, ssh, instance, custom), и self-managed установка всей платформы официально поддерживается.

# .gitlab-ci.yml
stages: [lint, test, build, deploy]

variables:
  GO_VERSION: "1.23"
  DOCKER_BUILDKIT: "1"
  # Кэш модулей внутри рабочей директории — иначе GitLab его не заархивирует
  GOPATH: "$CI_PROJECT_DIR/.go"

default:
  image: golang:1.23-alpine
  interruptible: true          # аналог cancel-in-progress
  retry:
    max: 2
    when: [runner_system_failure, stuck_or_timeout_failure]  # НЕ script_failure

.go_cache: &go_cache
  cache:
    key:
      files: [go.sum]          # ключ инвалидируется при смене зависимостей
    paths: [.go/pkg/mod, .cache/go-build]
    policy: pull               # большинство заданий только читают кэш

lint:
  stage: lint
  <<: *go_cache
  image: golangci/golangci-lint:v1.62
  script: [golangci-lint run --timeout 5m]

test:
  stage: test
  <<: *go_cache
  cache:
    key: { files: [go.sum] }
    paths: [.go/pkg/mod, .cache/go-build]
    policy: pull-push          # это задание кэш и наполняет
  services:
    - name: postgres:16-alpine
      alias: db
  variables:
    POSTGRES_PASSWORD: test
    DATABASE_URL: "postgres://postgres:test@db:5432/postgres?sslmode=disable"
  script:
    - apk add --no-cache gcc musl-dev
    - go test -race -coverprofile=cover.out ./...
    - go tool cover -func=cover.out | tail -1
  coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'   # покрытие в MR-виджет
  artifacts:
    when: always
    reports:
      junit: report.xml        # тесты отображаются во вкладке MR
    expire_in: 1 week

build:image:
  stage: build
  needs: [lint, test]          # DAG: не ждём всю стадию целиком
  image: gcr.io/kaniko-project/executor:debug
  script:
    # Kaniko собирает образ без демона Docker и без привилегированного режима
    - /kaniko/executor
      --context "$CI_PROJECT_DIR"
      --dockerfile Dockerfile
      --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
      --cache=true --cache-ttl=168h
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_COMMIT_TAG'

deploy:prod:
  stage: deploy
  needs: ["build:image"]
  environment:
    name: production
    url: https://app.example.com
  when: manual                 # ручной гейт
  rules:
    - if: '$CI_COMMIT_TAG'
  script: [./deploy.sh "$CI_COMMIT_TAG"]

Несколько вещей, за которые GitLab CI любят:

  • rules заметно выразительнее, чем only/except и чем if: в GitHub: можно комбинировать условия ветки, изменённых файлов (changes:), переменных и задавать when: manual/never/delayed прямо в правиле.
  • include и CI Components: пайплайн можно собрать из кусков, лежащих в другом репозитории, с версионированием через каталог компонентов (документация).
  • Parent-child pipelines — для монорепозиториев это лучше динамических матриц: родительский пайплайн генерирует YAML, дочерние запускаются независимо и имеют свои собственные статусы.
  • Отчёты: artifacts:reports умеет junit, coverage, SAST, dependency scanning — всё это подтягивается прямо в виджет merge request.

Регистрация своего раннера — три команды, и это принципиально проще, чем поднимать Jenkins-агента:

# Раннер на своей машине, Docker executor
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "glrt-XXXXXXXXXXXX" \
  --executor "docker" \
  --docker-image "alpine:3.20" \
  --docker-volumes "/cache" \
  --description "hetzner-cpx41-01"

# Проверяем
$ sudo gitlab-runner list
Runtime platform      arch=amd64 os=linux pid=3120 revision=v17.5.2
hetzner-cpx41-01      Executor=docker Token=glrt-XXXX URL=https://gitlab.com/

Слабые стороны: self-managed GitLab тяжёл (Rails-монолит, PostgreSQL, Redis, Gitaly, Sidekiq — реалистичный минимум 8 ГБ RAM на небольшую инсталляцию); YAML-якоря и extends дают запутанные наследования; лицензионные функции (например, окружения с approvals, multi-project pipelines в полном виде) сидят в Premium/Ultimate.


Drone и Woodpecker: минимализм

Drone был первым, кто довёл идею «шаг = контейнер» до предела: сервер — один Go-бинарник плюс база (SQLite/Postgres), раннер — ещё один бинарник. Всё.

Важный юридический нюанс: после покупки Harness Drone перелицензирован под Polyform Small Business — бесплатно только для компаний менее 100 сотрудников и с выручкой менее 1 $ млн. Поэтому сообщество ушло в форк Woodpecker CI (Apache 2.0), который сегодня активнее развивается (woodpecker-ci.org).

# .woodpecker.yml — синтаксис почти совместим с .drone.yml
when:
  - event: [push, pull_request]

steps:
  - name: lint
    image: golangci/golangci-lint:v1.62
    commands:
      - golangci-lint run --timeout 5m

  - name: test
    image: golang:1.23
    environment:
      DATABASE_URL: "postgres://postgres:test@db:5432/postgres?sslmode=disable"
    commands:
      - go test -race ./...

  - name: build-push
    image: woodpeckerci/plugin-docker-buildx
    settings:
      repo: registry.example.com/app
      tags: [latest, "${CI_COMMIT_SHA}"]
      username: { from_secret: registry_user }
      password: { from_secret: registry_pass }
    when:
      - event: push
        branch: main

services:
  - name: db
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: test

Обратите внимание: нет отдельного понятия «шаг-скрипт» против «шаг-плагин». Плагин — это просто образ, которому в переменные окружения передали settings. Написать свой плагин = собрать образ с одним бинарником. Это радикально прозрачнее, чем action.yml с composite-шагами.

Развернуть Woodpecker целиком:

# docker-compose.yml — весь CI-сервер
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v2
    ports: ["8000:8000"]
    volumes: ["woodpecker-data:/var/lib/woodpecker"]
    environment:
      WOODPECKER_OPEN: "false"
      WOODPECKER_HOST: "https://ci.example.com"
      WOODPECKER_GITEA: "true"
      WOODPECKER_GITEA_URL: "https://git.example.com"
      WOODPECKER_GITEA_CLIENT: "${GITEA_CLIENT}"
      WOODPECKER_GITEA_SECRET: "${GITEA_SECRET}"
      WOODPECKER_AGENT_SECRET: "${AGENT_SECRET}"

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v2
    command: agent
    depends_on: [woodpecker-server]
    volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
    environment:
      WOODPECKER_SERVER: "woodpecker-server:9000"
      WOODPECKER_AGENT_SECRET: "${AGENT_SECRET}"
      WOODPECKER_MAX_WORKFLOWS: "4"

volumes:
  woodpecker-data:

Ресурсы сервера — порядка 100 МБ RAM. Для сравнения: GitLab Omnibus на той же нагрузке съест 6–8 ГБ. Если у вас Gitea/Forgejo и десяток репозиториев, разница между «CI за 100 МБ» и «CI за 8 ГБ» — это разница между VPS за €5 и €30 в месяц.

Слабые стороны: нет marketplace сопоставимого масштаба, отчёты о тестах примитивные, нет встроенных окружений и approvals, матрицы есть, но без выразительности GitHub. Это инструмент для команд, которые хотят понятный YAML и полный контроль, а не батарейки в комплекте.


Остальные игроки — коротко и по делу

Платформа Модель исполнения Кому подходит Главный подвох
CircleCI SaaS, свои образы, orbs как переиспользуемые пакеты командам, где важны скорость и test splitting биллинг в «кредитах», сложно предсказать счёт
Buildkite control plane SaaS, агенты только ваши компаниям со своим железом и строгим комплаенсом платите за пользователей, инфраструктура целиком на вас
Azure Pipelines SaaS + self-hosted агенты .NET-миру, гибридным сценариям с Azure YAML-схема многословна; см. Azure
TeamCity сервер + агенты, конфиг в UI или в Kotlin DSL сложным сборкам JVM, монорепозиториям лицензия по агентам; Kotlin DSL — сильная сторона и барьер
Tekton CRD в Kubernetes, шаг = контейнер в поде платформенным командам, строящим свой внутренний CI «конструктор», а не продукт: UI, триггеры, RBAC — ваша работа
Argo Workflows DAG-движок в Kubernetes ML- и data-пайплайнам, длинным задачам не заточен под VCS-события; см. GitOps
Forgejo/Gitea Actions совместимость с синтаксисом GitHub Actions тем, кто хочет уйти с GitHub, не переписывая YAML поддерживается подмножество actions; marketplace надо зеркалить
Concourse всё есть ресурс, полностью stateless любителям строгой модели и воспроизводимости крутая кривая обучения, сообщество сжалось

Отдельно про Dagger: это не платформа, а способ описать пайплайн кодом (Go/Python/TypeScript), который потом исполняется контейнерами на любой платформе. Решает главную боль — «пайплайн можно проверить только в CI». С Dagger вы запускаете тот же граф локально. Цена — ещё один слой абстракции и BuildKit-демон.


Сравнение по трём осям, которые реально решают

Не «какая платформа лучше», а «что вы платите». Три валюты: деньги, время инженеров на эксплуатацию, время на обучение команды.

Стоимость: считаем, а не гадаем

Порядок цен на середину 2025 года (обязательно сверяйтесь с актуальным прайсом — тарифы меняются каждый год):

Позиция Цена Комментарий
GitHub Actions, Linux 2 vCPU 0.008 $/мин публичные репозитории — бесплатно
GitHub Actions, Linux 4 vCPU 0.016 $/мин larger runners не тратят бесплатный лимит
GitHub Actions, Windows 2 vCPU 0.016 $/мин ровно ×2 к Linux
GitHub Actions, macOS 3 vCPU 0.08 $/мин ×10 — macOS почти всегда узкое место бюджета
Бесплатный лимит GitHub Free / Team 2 000 / 3 000 мин/мес только для приватных репозиториев
GitLab SaaS, бесплатный тариф 400 compute-минут/мес сильно урезали в 2023
GitLab Premium 29 $/польз./мес + 10 000 минут доп. минуты ~10 $ за 1 000
Свои раннеры в GitLab 0 compute-минут считается только железо
Hetzner CCX23 (4 выделенных vCPU) ≈ €24/мес круглосуточно, без лимита минут
Hetzner CPX41 (8 vCPU shared) ≈ €26/мес хорошее соотношение для сборок

Точка безубыточности своих раннеров считается тривиально:

$$M_{\text{безуб}} = \frac{C_{\text{железо}} + C_{\text{эксплуатация}}}{p_{\text{минута}}}$$

Подставим: сервер за $26, обслуживание 2 часа инженера в месяц по 50 $/ч = $100, цена SaaS-минуты 4 vCPU $0.016.

$$M_{\text{безуб}} = \frac{26 + 100}{0.016} = 7,875 \text{ минут в месяц}$$

Если считать только железо (раннер поднят один раз через Terraform и живёт сам) — порог падает до 1 625 минут. Между этими двумя числами лежит вся честность разговора.

Точка окупаемости своих раннеров

Практический вывод: до ~1 500 минут в месяц самостоятельный хостинг раннеров не окупается никогда. Команда из пяти человек с пайплайном на 6 минут и 40 сборками в день — это ~1 200 минут. SaaS дешевле, спорить не о чем. А вот монорепозиторий с матрицей на 12 ячеек, где один прогон стоит 90 минут суммарного времени, выходит на 20–40 тысяч минут — там свои раннеры экономят тысячи долларов в год.

Отдельная строка — macOS. Один прогон iOS-сборки на 20 минут стоит $1.6. Тысяча сборок в месяц = 1 $ 600, что дороже, чем купить Mac mini M4 и амортизировать его за год. Это едва ли не единственный случай, когда «своё железо» окупается почти мгновенно.

Не забудьте и про хранилище: артефакты и кэш в GitHub тарифицируются (0.008 $/ГБ/день сверх лимита). Пайплайн, который каждый прогон кладёт 500 МБ артефактов с retention 90 days, за месяц накапливает десятки гигабайт. Ставьте retention-days: 7 для всего, кроме релизных артефактов, — это бесплатная экономия.

Где на самом деле уходит время сборки

Стоимость минут — половина картины. Вторая — сколько минут вы тратите на накладные расходы, а не на полезную работу.

Разбор времени одной сборки

Три вывода из этой картинки:

  1. Холодный старт стоит ~40 секунд на каждое задание. Матрица из 12 ячеек по 3 задания — это 24 минуты чистых накладных расходов за прогон. Дробить пайплайн на мелкие задания «для красоты DAG» — дорого.
  2. Кэш через сеть медленнее локального диска в разы. GitHub Actions cache даёт порядка 10 лимитов на репозиторий; при большом node_modules восстановление занимает столько же, сколько установка с нуля. Меряйте, прежде чем добавлять шаг кэша.
  3. CPU у SaaS-раннеров слабый. Стандартный ubuntu-latest — это 2 vCPU shared. Выделенные 4 ядра на Hetzner дают компиляцию в 2+ раза быстрее. Если задание CPU-bound, переход на свои раннеры сокращает время ожидания разработчика, а это дороже минут.

Как выбирать: рабочий алгоритм

Главное правило: CI-платформа должна быть там же, где код. Комбинация «код на GitHub, CI в Jenkins» или «GitLab плюс отдельный Drone» почти всегда добавляет боли (двойная аутентификация, рассинхрон статусов, вебхуки, которые ломаются) без соразмерной выгоды. Исключения бывают — например, Buildkite при коде на GitHub, когда нужны свои агенты и очень жёсткий комплаенс, — но это осознанное решение, а не «так исторически сложилось».


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

1. Секреты в логах. Платформы маскируют значения секретов, но только буквальное совпадение. Секрет, прошедший через base64 или попавший в JSON с экранированием, утечёт в открытом виде. Правило: set -x в шаге, который трогает секреты, — запрещён.

2. pull_request_target без понимания. В GitHub Actions обычный pull_request от форка не получает секретов — это защита. Разработчик натыкается на «мой workflow не видит токен», гуглит, находит pull_request_target и меняет триггер. Теперь workflow исполняется в контексте базовой ветки с полными секретами, а если он ещё и делает checkout кода из PR — любой человек в интернете может выполнить свой код с вашими ключами. Это самая распространённая критическая уязвимость GitHub Actions (разбор GitHub Security Lab).

3. Ретраи на script_failure. Соблазн поставить retry: 2 на всё огромен — «флаки же». В результате настоящие баги маскируются, а время сборки растёт втрое. Ретраить можно только инфраструктурные отказы: runner_system_failure, таймауты, сетевые ошибки. Флакающий тест надо чинить или карантинить явно.

4. Кэш как источник правды. Кэш может исчезнуть в любой момент (вытеснение по объёму, смена ключа, зачистка). Пайплайн, который падает без кэша, сломан. Кэш ускоряет, но не является зависимостью.

5. Один гигантский job. Соблазн: всё в одном задании, ничего не надо передавать. Проблема: упало на 18-й минуте — перезапускаете все 18. Разумный компромисс — 3–6 заданий, разделённых по естественным границам (линт / тесты / сборка образа / деплой), а не по каждой команде.

6. latest в образах раннера и сервисов. postgres:16-alpine — воспроизводимо. postgres:latest — однажды утром ваши тесты сломает мажорное обновление, и вы потратите день на «почему упало, я же ничего не менял».

7. Отсутствие таймаутов. Значение по умолчанию у GitHub — 360 минут. Зависшее задание съест шесть часов оплаченного времени. Ставьте timeout-minutes: 15 явно на каждое задание.

8. Игнорирование стоимости хранения. Проверьте счёт за artifacts/packages, прежде чем оптимизировать минуты, — нередко хранилище стоит дороже вычислений.


Практика продакшена: что делают зрелые команды

  • Пиннинг и аудит зависимостей пайплайна. Все внешние actions — по SHA, обновление через Dependabot/Renovate с ревью. Внутренние переиспользуемые workflow — в отдельном репозитории с тегами.
  • Собственный слой абстракции. Пайплайн вызывает make ci-test, make ci-build, а не двадцать строк YAML. Это делает миграцию между платформами вопросом одного дня и позволяет разработчику воспроизвести CI локально.
  • Автомасштабируемые раннеры. GitHub — actions-runner-controller в Kubernetes; GitLab — docker-autoscaler executor c AWS/Hetzner. Эфемерный раннер на spot-инстансе живёт минуты и даёт двойную выгоду: цена и изоляция (никакого состояния между сборками).
  • Метрики самого пайплайна. DORA-метрики (частота деплоя, lead time, MTTR, доля неудачных изменений) плюс p50/p95 времени сборки и процент флаков. Без цифр «CI стал медленным» — эмоция, а не задача. Подробности — в статье про наблюдаемость.
  • Бюджет времени сборки как SLO. Например: «p95 пайплайна на PR ≤ 10 минут». Превысили — заводим задачу. Без явного порога время сборки растёт монотонно и необратимо.
  • Отдельные раннеры для защищённых веток. Задания, у которых есть доступ к продакшн-секретам, идут на выделенный пул раннеров с ограниченным сетевым доступом. Сборка PR от внешнего контрибьютора и деплой в прод не должны делить один хост.

Мини-итог

  • Современные платформы CI отличаются от Jenkins не синтаксисом, а моделью: конфиг в репозитории, control plane без состояния, раннеры сами забирают работу.
  • GitHub Actions — лучший старт и лучшая экосистема; платите порогом безопасности (чужие actions) и слабым языком выражений.
  • GitLab CI — самая выразительная модель пайплайна (rules, needs, include, parent-child) и бесплатные свои раннеры; платите весом self-managed инсталляции.
  • Woodpecker/Drone — минимализм, который честно работает на малом масштабе; платите отсутствием батареек.
  • Tekton/Argo — не продукты, а конструкторы; берите, только если строите внутреннюю платформу.
  • Считайте деньги по формуле безубыточности: до ~1 500 минут в месяц свои раннеры не окупаются, после 8 000 — окупаются почти всегда. macOS — исключение, там своё железо выгодно сразу.
  • Держите бизнес-логику сборки в скриптах, а не в YAML: это ваша страховка от вендор-лока.

Источники

Что дальше

Мы научились быстро и дёшево доводить коммит до собранного, проверенного и опубликованного артефакта. Следующий вопрос — как этот артефакт попадает к пользователям без простоя и с возможностью мгновенно откатиться: Continuous Delivery и стратегии релизов: blue-green, canary, feature flags, откаты.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов