GitLab CI vs GitHub Actions: что выбрать для автоматизации в 2026

CI/CD в 2026: почему выбор платформы всё ещё важен

Сколько раз слышал: "они же одинаковые, просто YAML по-разному". Не одинаковые. GitLab CI - часть платформы, где живёт весь рабочий процесс. GitHub Actions вырос из экосистемы с упором на Marketplace. Для VPS и dedicated-серверов эта разница ощущается конкретно: в том, как настраиваются раннеры, что происходит с секретами и насколько больно будет мигрировать через год.

Здесь только практика - конфиги, которые реально работают, и ситуации, где один инструмент выигрывает у другого.

GitLab CI: как устроена система

GitLab CI не отдельный сервис - он встроен в платформу. Конфигурация живёт в .gitlab-ci.yml в корне репозитория. Пайплайн - это набор стадий (stages), каждая содержит джобы (jobs), их выполняют раннеры.

Архитектура раннеров

Здесь и есть суть - то, чем GitLab CI отличается на практике. Два типа раннеров:

  • Shared runners - инфраструктура GitLab.com. Бесплатный план даёт 400 минут в месяц, и это мало - средний Node.js проект с тестами сжирает 150-200 минут за неделю.
  • Self-hosted runners - агент на вашем сервере. Никаких лимитов, данные не уходят наружу.

Установка self-hosted раннера занимает минут десять, не больше: скачиваете бинарник gitlab-runner, регистрируете токеном из настроек репозитория, выбираете executor - shell, docker или kubernetes. Раннер работает по polling-протоколу, то есть сам опрашивает GitLab. Входящих соединений не нужно, только исходящий HTTPS - это важно, когда VPS за NAT или файрволом.

Синтаксис и возможности

Из коробки GitLab CI умеет needs для DAG-зависимостей между джобами, include для внешних конфигов, extends для наследования, rules для условной логики. Всё нативно - без плагинов. Встроенный container registry, пакетный реестр, Kubernetes-интеграция идут в комплекте с платформой.

GitHub Actions: как устроена система

Конфиги лежат в .github/workflows/*.yml. Концепции простые: workflow - рабочий процесс, job - набор шагов, step - отдельное действие. Шаги либо берут готовое действие из Marketplace, либо выполняют произвольные команды через run:. Порог входа ниже, чем у GitLab - если раньше не работал с CI/CD, GitHub Actions освоишь быстрее.

Marketplace как главное преимущество

В 2026 году в GitHub Marketplace больше 20 000 готовых actions. Деплой на AWS, Terraform, уведомления в Slack, сканирование уязвимостей - для большинства стандартных задач что-то уже есть. Обратная сторона: ты зависишь от сторонних авторов. Action популярного репо могут перестать поддерживать, и придётся искать замену или форкать.

Раннеры GitHub Actions

Hosted runners - ubuntu-latest, windows-latest, macos-latest. Для публичных репозиториев минуты не ограничены. Для приватных на бесплатном тарифе - 2000 минут в месяц, что уже лучше, чем у GitLab. Self-hosted раннеры есть, но они менее гибко настраиваются - GitLab в этом плане даёт больше контроля над executor-ами.

Прямое сравнение

Параметр GitLab CI GitHub Actions
Бесплатные минуты (приватные репо) 400 мин/мес (GitLab.com Free) 2000 мин/мес (GitHub Free)
Self-hosted runners Отличная поддержка, gitlab-runner daemon, множество executor-ов Поддерживаются, меньше опций конфигурации
Self-hosted платформа целиком Да, GitLab CE/EE - полный self-host Только через GitHub Enterprise Server
Управление секретами CI/CD Variables, маскировка, защищённые переменные по веткам Secrets на уровне репо/org, environments с защитой
Container Registry Встроен в платформу GitHub Container Registry (ghcr.io)
Синтаксис пайплайна stages + jobs, DAG через needs, YAML anchors jobs + steps, матричные сборки, reusable workflows
Маркетплейс/плагины Нет маркетплейса, всё через встроенные фичи 20 000+ actions в Marketplace
Интеграция с issue/MR/PR Нативная, включая Auto DevOps Нативная через GitHub API
Артефакты и кэш Встроены, хранятся на GitLab actions/cache, actions/upload-artifact
Kubernetes-интеграция Встроенная, GitLab Agent for Kubernetes Через сторонние actions или kubectl

Практические примеры: деплой на VPS по SSH

Задача стандартная: пуш в main - сборка - тесты - деплой на VPS через SSH. Вот оба варианта в реальных конфигах.

GitLab CI - деплой на VPS

Node.js-приложение. Приватный SSH-ключ хранится в переменной SSH_PRIVATE_KEY, IP сервера в DEPLOY_HOST, пользователь в DEPLOY_USER.

# .gitlab-ci.yml

stages:
  - test
  - build
  - deploy

variables:
  NODE_ENV: production

test:
  stage: test
  image: node:20-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

build:
  stage: build
  image: node:20-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
    policy: pull
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client rsync
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
  script:
    - rsync -az --delete dist/ ${DEPLOY_USER}@${DEPLOY_HOST}:/var/www/app/
    - ssh ${DEPLOY_USER}@${DEPLOY_HOST} "
        cd /var/www/app &&
        pm2 restart app --update-env ||
        pm2 start ecosystem.config.js"
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  needs:
    - job: build
      artifacts: true

needs запускает деплой сразу после сборки, не ждёт остальных джобов на той же стадии. rules - замена старому only/except, логика гибче. Артефакт dist/ передаётся между джобами автоматически, прописывать ничего лишнего не нужно.

GitHub Actions - тот же деплой

Секреты - Settings, раздел Secrets and variables, вкладка Actions.

# .github/workflows/deploy.yml

name: Test, Build and Deploy

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install and build
        run: |
          npm ci
          npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 1

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://example.com
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy via rsync
        run: |
          rsync -az --delete dist/ \
            ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/var/www/app/

      - name: Restart application
        run: |
          ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} "
            cd /var/www/app &&
            pm2 restart app --update-env ||
            pm2 start ecosystem.config.js"

Синтаксис похож, но есть принципиальное отличие: GitHub Actions требует явно загружать и скачивать артефакты через отдельные шаги. Секреты передаются через ${{ secrets.NAME }}. SSH-агент из коробки не настраивается - либо пишете руками, либо берёте сторонний action типа webfactory/ssh-agent. Именно это ломает ощущение "просто настроить" в первый раз.

Self-hosted runner в GitLab: деплой без исходящего SSH

Если GitLab Runner стоит прямо на целевом сервере или в той же сети - SSH вообще не нужен. Деплой упрощается до минимума.

# .gitlab-ci.yml (runner запущен на целевом сервере)

deploy:
  stage: deploy
  tags:
    - production-vps
  script:
    - cp -r dist/* /var/www/app/
    - systemctl restart myapp
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  needs:
    - job: build
      artifacts: true

Тег production-vps отправляет джоб на конкретный раннер. По-честному, это одно из главных архитектурных преимуществ GitLab CI, когда работаешь с собственной инфраструктурой.

Когда выбирать GitLab CI

Главная причина - self-hosted инфраструктура. Если код не должен уходить на серверы GitLab.com, GitLab CE разворачивается на вашем VPS и закрывает весь стек: git-хостинг, CI/CD, container registry, issue tracker. GitHub аналогичного без Enterprise лицензии не даёт, это не мнение - просто факт из прайса.

Ещё один повод - интенсивные пайплайны на приватных репо. 400 минут в месяц от GitLab.com заканчиваются примерно к середине второй недели при активной разработке. Self-hosted раннер решает это раз и навсегда.

Сложные пайплайны с needs, динамические child pipelines, встроенный registry - всё это в GitLab CI реализовано нативно, без плагинов и workaround-ов. Если команда уже живёт внутри GitLab-экосистемы - смена CI просто ничего не даст.

Когда выбирать GitHub Actions

Open source - без вопросов. Неограниченные минуты на публичных репо плюс огромное сообщество, которое уже написало actions на все случаи жизни. Деплой на AWS, сканирование зависимостей, публикация npm-пакета - находишь нужный action, вставляешь в workflow, готово.

Для небольших команд с проектами на GitHub и несложными пайплайнами - GitHub Actions явно проще в старте. Не нужно поднимать отдельный сервер для раннера, не нужно разбираться в executor-ах. Написал workflow за вечер, и он работает.

Матричные сборки читаются лучше, чем в GitLab - это субъективно, но факт. Тестировать на трёх версиях Node и двух ОС в GitHub Actions настраивается декларативно и понятно.

Безопасность деплоя на VPS

Одно правило, которое одинаково работает на обеих платформах: отдельный SSH-ключ для деплоя с правами только на нужную директорию и перезапуск конкретного сервиса через sudoers. Не root-доступ, не полный sudo - только то, что нужно деплой-скрипту.

В GitLab CI деплой-переменные помечайте как Protected - тогда они видны только в защищённых ветках. В GitHub Actions для production используйте environments с required reviewers. Self-hosted раннер под root не запускайте - Docker executor с изоляцией надёжнее.

Deploy-ключи нужно ротировать. Обе платформы об этом не напоминают вообще никак.

Практическая рекомендация

Если у вас собственные серверы и приватный код - GitLab CI с self-hosted раннером. GitLab CE на VPS от 4 GB RAM, и всё необходимое под рукой без зависимости от чужих сервисов. Миграция с GitLab.com на self-hosted GitLab - это одна команда для экспорта проекта.

Если проект на GitHub и CI/CD не сложнее сборки и деплоя - GitHub Actions. Для open source особенно: 2000 минут в месяц на приватных репо и безлимит на публичных при нулевой настройке инфраструктуры.

Мигрировать между платформами неприятно: YAML несовместим, секреты переносить вручную, раннеры перенастраивать. Поэтому выбор лучше делать под долгосрочный план, а не под то, что проще поднять сегодня за час.