Git в CI/CD: shallow, кеш, merge_group и безопасность

Рейтинг: 80.6% · 16 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Git в CI/CD: shallow, кеш, merge_group и безопасность

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность (вы здесь)
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Локально твой репозиторий жирный и полный: вся история, все ветки, все теги. В CI картина другая. Раннер - это чистая виртуалка, которая поднимается с нуля, клонирует код, делает свою работу и умирает. И вот тут начинается интересное: то, что у тебя на ноуте работает мгновенно, в пайплайне внезапно валится с ошибкой про "no merge base" или печатает версию вида 0.0.0-unknown вместо аккуратного тега. Виноват не Git и не CI по отдельности - виновато непонимание того, ЧТО именно Git притаскивает в раннер и какими правами он там оперирует.

Этот урок - про git ci cd с инженерной стороны. Разберём, почему shallow clone ci быстрый, но коварный; как кешировать .git между джобами; почему в CI ты всегда в detached HEAD и как достать имя ветки; что такое событие merge_group и зачем гонять проверки в merge queue ДО вливания; и главное - как не спалить токены в логах и уйти от долгоживущих секретов через OIDC. Всё это - фундамент trunk-based разработки, где десятки коротких веток вливаются в main по много раз в день.

fetch-depth и shallow clone: почему CI клонирует не всё

Когда ты делаешь git clone у себя, Git тащит весь граф коммитов. В CI это расточительно: репозиторий монорепы может весить гигабайты, а джобе нужен ровно один коммит - тот, что запустил пайплайн. Поэтому actions/checkout по умолчанию делает shallow clone глубиной 1. Под капотом это git fetch с флагом --depth=1.

Код: Выделить всё

$ git clone --depth=1 https://github.com/acme/app.git
$ cd app
$ git log --oneline
a1b2c3d (HEAD -> main, grafted, origin/main) fix: race in worker
$ cat .git/shallow
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Ключевое слово в выводе - grafted. Git помечает коммит как "привой": он притворяется корнем истории, у которого нет родителей, хотя на самом деле родители есть - просто их не скачали. Список таких обрезанных точек лежит в файле .git/shallow. Пока ты собираешь и тестишь HEAD - всё отлично, экономия огромная. Проблемы начинаются, когда команде нужна ИСТОРИЯ.
  • git blame на shallow покажет, что чуть ли не каждую строку написал один последний коммит - потому что предков, где строки реально менялись, в репозитории нет.
  • git describe не найдёт ближайший тег и либо упадёт, либо выдаст мусор - теги при depth=1 обычно не приезжают вовсе.
  • git merge-base origin/main HEAD вернёт "fatal: no merge base" или "no common commits" - точки расхождения веток нет в графе. На этом ломаются почти все changed-files экшены, которые считают diff между ветками.
Лечение прямое: говоришь CI тащить полную историю.

Код: Выделить всё

# .github/workflows/ci.yml
- uses: actions/checkout@v5
  with:
    fetch-depth: 0   # 0 = вся история и все теги, НЕ нулевая глубина
fetch-depth: 0 - это не "ноль коммитов", а специальное значение "снять ограничение глубины". В git gitlab ci аналог - переменная GIT_DEPTH: 0 или настройка Git shallow clone в UI проекта (там дефолт обычно 20-50). Практическое правило: джобам сборки и юнит-тестов хватает depth=1, а вот линтерам коммитов, семантик-релизу, любым diff-по-веткам и инструментам версионирования нужен fetch-depth: 0. Не ставь 0 везде "на всякий случай" - на большой монорепе это десятки секунд лишнего клонирования на каждой джобе.

Изображение

Кеширование .git и зависимостей между джобами

Раннер эфемерен, но клонировать гигабайты на каждый запуск глупо. Есть два механизма.

Первый - кеш зависимостей (node_modules, vendor, .m2, ~/.cargo). Это самое прибыльное: качаешь пакеты один раз, восстанавливаешь по хешу lock-файла.

Код: Выделить всё

- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/composer
      vendor
    key: deps-${{ hashFiles('composer.lock') }}
    restore-keys: deps-
Ключ кеша строится по хешу lock-файла: поменялись зависимости - поменялся хеш - кеш инвалидируется автоматически. Это золотое правило: ключ должен меняться ровно тогда, когда меняется содержимое.

Второй механизм - кеш самого .git. На GitHub Actions checkout уже умеет частично переиспользовать объекты, но на self-hosted раннерах и в git gitlab ci кеширование папки .git между пайплайнами реально ускоряет fetch: вместо полного клона Git докачивает только новые объекты. Здесь же на 2026 хорошо ложится partial clone - actions/checkout умеет filter: blob:none, и Git притащит граф коммитов без содержимого файлов, дотягивая блобы по требованию. Для джоб, которым нужна история коммитов, но не все версии всех файлов (тот же commitlint), это лучший компромисс между depth=1 и fetch-depth: 0.

Код: Выделить всё

- uses: actions/checkout@v5
  with:
    filter: blob:none   # граф есть, blame/merge-base работают, блобы - лениво
Detached HEAD в CI и как достать имя ветки

Запусти в пайплайне git status и удивишься:

Код: Выделить всё

$ git status
HEAD detached at a1b2c3d
nothing to commit, working tree clean
$ git rev-parse --abbrev-ref HEAD
HEAD
CI почти всегда чекаутит конкретный SHA, а не ветку - так детерминированнее. Поэтому HEAD "отцеплен": он указывает прямо на коммит, минуя ярлык ветки. И git rev-parse --abbrev-ref HEAD, который локально выдаёт имя ветки, в CI честно отвечает "HEAD" - потому что ветки тут нет, есть голый коммит. Скрипты, которые завязаны на имя текущей ветки, в этот момент ломаются.

Правильный путь - брать имя ветки не из Git, а из переменных окружения CI, который точно знает контекст события:

Код: Выделить всё

# GitHub Actions
echo "$GITHUB_REF_NAME"        # feature/login (для push)
echo "$GITHUB_HEAD_REF"        # ветка-источник PR (для pull_request)

# GitLab CI
echo "$CI_COMMIT_REF_NAME"
echo "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
Запомни различие в GitHub: для события push имя в GITHUB_REF_NAME, а для pull_request там окажется что-то вроде 42/merge (номер PR), и реальная ветка-источник - в GITHUB_HEAD_REF. Путаница тут - классический источник багов в релизных скриптах.

Событие merge_group: проверки в merge queue до вливания

Теперь к 2026-новинке для git github actions, которая прямо завязана на trunk-based. Представь десять PR, каждый зелёный сам по себе, протестированный против main, который был час назад. Их вливают подряд - и main падает: PR номер 3 удалил функцию, которую PR номер 7 начал использовать. Каждый по отдельности валиден, вместе - нет. Это называется semantic merge conflict, и обычный required check его не ловит.

Решает это merge queue. Когда PR одобрен, он встаёт в очередь. GitHub берёт текущий main, накатывает поверх изменения из очереди и создаёт временную ветку-кандидат gh-readonly-queue/main/.... Для неё стреляет событие merge_group. CI прогоняется уже на реальной комбинации "main плюс всё, что вливается" - и только если зелено, изменения попадают в main. Красный кандидат вылетает из очереди, не сломав ствол.

Главный подвох, на котором спотыкаются все: если твой workflow триггерится только на pull_request, то в merge queue он не запустится, и required-проверка зависнет навечно. Нужно явно добавить триггер:

Код: Выделить всё

on:
  pull_request:
  merge_group:
    types: [checks_requested]
В 2026 merge queue и required checks настраиваются через repository rulesets - GitHub уводит всех с classic branch protection на ruleset. В ruleset включаешь "Require merge queue" и список обязательных проверок. Аналог в GitLab - Merge Trains: тот же принцип последовательной проверки кандидатов на актуальном target. Без merge queue на активном trunk-based репозитории ты обречён на регулярные красные main и ручные ревёрты.

Безопасность: токены, OIDC и git токен безопасность ci

Самая дорогая ошибка в CI - утечка секрета. И Git к ней причастен напрямую.

Не свети credentials в логах. Классика - токен в URL ремоута. Если сделать так, он окажется в выводе и осядет в архиве логов пайплайна:

Код: Выделить всё

# ПЛОХО: токен попадёт в логи при ошибке или set -x
git push https://x-access-token:${TOKEN}@github.com/acme/app.git
Лучше прокидывать токен через заголовок, который Git не печатает, или через credential helper. GitHub маскирует значения секретов в логах звёздочками, но маскировка спасает только от точного совпадения строки - стоит токену пройти через base64 или попасть в URL по кускам, и маскировка его не узнает.

Минимальные права токена. GITHUB_TOKEN по умолчанию выдаётся на джобу с правами, и их надо урезать до необходимого. Тот же принцип для CI_JOB_TOKEN в GitLab.

Код: Выделить всё

permissions:
  contents: read      # по умолчанию - только чтение
  pull-requests: write  # добавляем точечно, что реально нужно
OIDC вместо долгоживущих секретов. Хранить ключи AWS в секретах CI - это бомба замедленного действия: ключ не ротируется, утечёт - будут майнить крипту на твоём аккаунте. Современный путь - keyless через OIDC. Workflow запрашивает у GitHub короткоживущий подписанный JWT с claims о контексте (репо, ветка, окружение), отдаёт его в AWS STS, тот проверяет подпись по публичным ключам GitHub и сверяет claims со своей trust policy. Совпало - выдаёт временные креды на минуты. Никаких ключей в хранилище вообще.

Код: Выделить всё

permissions:
  id-token: write     # без этого GitHub не выдаст OIDC-токен
  contents: read
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
      aws-region: eu-central-1
Права id-token: write обязательны, иначе токен не выдаётся. Тот же механизм работает GitHub OIDC -> GCP. Для подписи коммитов от ботов в CI логично взять Sigstore/gitsign: keyless-подпись по тому же OIDC, без приватных GPG-ключей на раннере - identity бота фиксируется в прозрачном логе.

Защита от инъекций. Никогда не подставляй пользовательский ввод (заголовок PR, имя ветки) прямо в shell-команду внутри Git-вызова - заголовок вида $(rm -rf /) выполнится. Прокидывай такие данные через переменные окружения, а не через интерполяцию в run.

Отключай хуки в CI. Раннер не должен исполнять хуки из репозитория - это и лишние действия, и вектор атаки. Современный безопасный дефолт - core.hooksPath, указывающий в пустоту, или флаг для одиночной команды:

Код: Выделить всё

git -c core.hooksPath=/dev/null commit -m "ci: bump version"
Типичные грабли
  • fetch-depth: 1 (дефолт) плюс git describe = версия-мусор. Релизные джобы всегда с fetch-depth: 0.
  • git merge-base падает с "no merge base" - забыл полную историю для diff между ветками.
  • merge_group не добавлен в triggers - проверка в merge queue висит pending вечно, PR не вливается.
  • Ключ кеша без хеша lock-файла - кеш протух и тащит старые зависимости, сборка зелёная на мусоре.
  • Токен в git remote URL - утёк в логи. Звёздочки маскировки не спасают от перекодировки.
  • Скрипт берёт ветку из git rev-parse в detached HEAD - получает строку "HEAD" вместо имени.
  • Долгоживущие AWS-ключи в секретах вместо OIDC - тикающая бомба при первой же утечке.
Мини-лаба: воспроизведи shallow-боль руками
  • Возьми любой репозиторий с историей и тегами. Сделай рядом shallow-копию: git clone --depth=1 file:///path/to/repo shallow и зайди в неё.
  • Глянь cat .git/shallow и git log --oneline - увидишь grafted на единственном коммите.
  • Запусти git describe --tags - получишь ошибку "No names found" или "cannot describe". Запусти git merge-base HEAD HEAD~5 - получишь fatal, предков нет.
  • Дотяни историю: git fetch --unshallow. Проверь, что .git/shallow исчез, а git describe и git log теперь работают.
  • Повтори через partial clone: git clone --filter=blob:none file:///path/to/repo partial. Сравни du -sh у обоих и убедись, что blame и merge-base тут работают сразу.
Контрольные вопросы
  • Почему git blame в shallow-клоне приписывает почти все строки одному коммиту и как это починить?
  • Workflow с required-проверкой настроен только on: pull_request. Что произойдёт, когда PR встанет в merge queue?
  • Чем OIDC-аутентификация в облако лучше хранения ключей доступа в секретах CI? Какое право в permissions для неё обязательно?
  • В CI ты в detached HEAD. Откуда брать имя ветки-источника для PR на GitHub и почему не из git rev-parse?
Итог

CI - это Git в спартанских условиях: эфемерный раннер, обрезанная история, чужие права. Держи в голове три рычага. Глубина: depth=1 для сборки, fetch-depth: 0 или blob:none там, где нужна история и теги. Очередь: merge_group гоняет проверки на реальной комбинации изменений до вливания в ствол - фундамент честного trunk-based. Безопасность: минимальные права токенов, никаких секретов в логах и URL, keyless OIDC вместо долгоживущих ключей. Сложи это - и пайплайн станет быстрым, а main перестанет краснеть от того, что два валидных PR не ужились вместе.
👍3 ❤️5 🔥3 😄 🤔
Аватара пользователя
laksjd
Сообщения: 1
Зарегистрирован: 07 июн 2026, 14:11

Re: Git в CI/CD: shallow, кеш, merge_group и безопасность

Сообщение laksjd »

Вот оно что, а я неделю воевал с git describe в релизной джобе - выдавал 0.0.0 и всё. Поставил fetch-depth: 0 и заработало. Спасибо, наконец понял ПОЧЕМУ.
👍 ❤️ 🔥2 😄 🤔
Аватара пользователя
pragmatism
Сообщения: 1
Зарегистрирован: 22 май 2026, 05:04

Re: Git в CI/CD: shallow, кеш, merge_group и безопасность

Сообщение pragmatism »

А правда что без merge_group в триггерах проверка просто висит pending? У нас как раз PR не вливаются и никто не понимает почему, пойду гляну ruleset
👍1 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
Огромные репозитории: partial clone, FSMonitor, Scalar
Следующая глава →
Куда движется Git: дорожная карта 3.0 и экосистема

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Разрешение конфликтов слияния в Gitgit merge или rebase: что выбратьgit merge: слияние ветокGit в CI/CD

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость