Все уроки
DevOps и облакоСредний12 мин чтенияВ моём стеке

Git, CI/CD и деплой без простоя

От коммита на вашем ноутбуке до безопасного и обратимого релиза в продакшене.

  • Git flow
  • Pull requests
  • Pipelines
  • Zero-downtime deploys
Шаг 1/4
main — ветка, которая всегда готова к релизу. Каждая точка — коммит.

За 30 секунд

Git записывает каждое изменение кода и позволяет команде параллельно работать в ветках. Continuous integration запускает проверки (линтер, тесты, сборку) на каждое изменение, так что проблемы ловятся за минуты, а не в продакшене. Continuous delivery затем выкатывает протестированную сборку на серверы так, что пользователи этого не замечают, а вы можете быстро всё откатить.

Почему я это использую

  • Я использую Git как общую память команды: у каждого изменения есть автор, причина и путь назад.
  • Я полагаюсь на автоматические проверки, потому что люди устают, а пайплайны — нет; красная сборка дешевле сломанной страницы оформления заказа.
  • Мне важен деплой без простоя, потому что клиенты принимают заказы и платежи круглосуточно, и удачного момента для страницы «ведутся технические работы» просто нет.
  • Маленькие, частые и обратимые релизы — самое действенное из известных мне средств от страха перед деплоем.

Как это работает

01Основы Git: коммиты, ветки, merge и rebase

Коммит — это снимок всего проекта плюс сообщение и указатель на родителя, так что история — это цепочка снимков. Ветка — просто подвижная метка, указывающая на коммит, поэтому ветки дешёвые. Когда две линии работы нужно свести вместе, вы делаете merge или rebase. Merge создаёт новый коммит с двумя родителями и сохраняет историю ровно такой, какой она была. Rebase переигрывает ваши коммиты поверх свежего main и даёт прямую линию, но переписывает эти коммиты с новыми хэшами. Правило: делайте rebase своей локальной или feature-ветки, чтобы навести порядок, но никогда не делайте rebase коммитов, которые другие уже стянули. Хорошие коммиты маленькие и сфокусированные, а их сообщение объясняет, зачем, а не только что.

bash
git switch -c feature/order-export
git add -p                       # stage hunks, review as you go
git commit -m "Add CSV export for orders"
git fetch origin
git rebase origin/main           # update your branch, resolve conflicts
git push --force-with-lease      # safe force-push of your own branch
git log --oneline --graph -15

02Стратегии ветвления: trunk-based и GitFlow

Trunk-based разработка держит одну основную ветку, которая всегда готова к релизу. Разработчики работают в короткоживущих ветках, которые в идеале вливаются за день-два, а незаконченные фичи прячут за feature flag'ами. Это сводит к минимуму болезненные мерджи и естественно сочетается с continuous deployment. GitFlow использует долгоживущие ветки develop и main плюс ветки feature, release и hotfix. Он подходит продуктам с версионированными релизами по расписанию — например, мобильным приложениям, проходящим ревью в сторах, или on-premise софту, где одновременно поддерживается несколько версий. Для большинства веб-приложений выигрывает trunk-based с короткими pull request'ами. Что бы вы ни выбрали, защитите main: требуйте pull request'ы, зелёные проверки и хотя бы одно ревью, запретите force-push.

  • Trunk-based: короткие ветки, feature flag'и, деплой из main.
  • GitFlow: ветки develop + release, хорош для версионированных релизов.
  • Релизные теги (v1.8.0) точно отмечают, что ушло в продакшен.

03Pull request'ы и код-ревью

Pull request — это предложение влить ветку плюс обсуждение вокруг него. Его задача — ловить баги, делиться знаниями и поддерживать единообразие кодовой базы, а не показывать, кто тут сеньор. Держите pull request'ы маленькими, в идеале до нескольких сотен строк: качество ревью резко падает с ростом размера. В описании должно быть, что изменилось, зачем и как это проверялось, а для UI — скриншоты. Стиль и форматирование пусть проверяет автоматика, чтобы люди могли сосредоточиться на логике, сохранности данных, безопасности, именовании и недостающих тестах. Ревьюеры должны быть конкретными и доброжелательными и отделять блокирующие замечания от предложений. Файлы CODEOWNERS направляют изменения людям, которые знают эту часть кода, а обязательные status check'и не дают ничего влить, пока CI красный.

04Continuous integration: пайплайн на каждое изменение

Continuous integration означает, что каждый push и pull request автоматически собирается и тестируется в чистом окружении. Типичный пайплайн ставит зависимости из lockfile, запускает линтер и проверку типов, гоняет unit- и feature-тесты и собирает продакшен-артефакт. Располагайте шаги от быстрых к медленным, чтобы большинство ошибок всплывало за минуту-две. Кэшируйте загрузку зависимостей между запусками. Пайплайн должен быть одинаковым шлагбаумом для всех: если он красный, изменение не вливается. Ниже — минимальный workflow GitHub Actions для Node- или Next.js-проекта; GitLab CI, Bitbucket Pipelines и другие используют очень похожие идеи с другим синтаксисом. Секреты живут в хранилище секретов CI-провайдера и никогда — в репозитории.

yaml
name: ci
on:
  pull_request:
  push:
    branches: [main]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: 24, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test
      - run: npm run build

05Стратегии деплоя: rolling, blue-green, canary

Continuous delivery означает, что любую зелёную сборку можно задеплоить одной кнопкой; continuous deployment — что она деплоится автоматически. Важно и то, как вы заменяете работающую версию. Rolling deploy заменяет инстансы по одному за load balancer'ом: мощность слегка проседает, но сервис продолжает работать. Blue-green держит два одинаковых окружения: вы деплоите в простаивающее (green), проверяете его, затем переключаете трафик на балансировщике или симлинком — и обратное переключение мгновенно. Canary отправляет на новую версию небольшую долю трафика, скажем пять процентов, следит за ошибками и задержками и постепенно расширяет долю. На одном сервере идею blue-green обычно реализуют через папки релизов и атомарное переключение симлинка или через запуск нового контейнера, который проходит health check, прежде чем на него переключат proxy.

  • Rolling: дёшево, постепенно, две версии работают одновременно.
  • Blue-green: мгновенное переключение и мгновенный откат, ненадолго нужна двойная мощность.
  • Canary: ограничивает радиус поражения, но нужны хорошие метрики, чтобы судить.

06Деплой без простоя для Laravel и Next.js

Для Laravel на VPS проверенный паттерн — атомарные релизы: каждый деплой уходит в новую папку с таймстемпом, общие .env и storage подключаются симлинками, там же ставятся зависимости и собираются кэши, и только потом симлинк current переключается одним атомарным шагом. Reload PHP-FPM очищает OPcache, а queue:restart говорит воркерам доделать текущую задачу и стартовать уже на новом коде. Для Next.js тоже собирайте в новую папку релиза, а затем плавно перезагружайте Node-процесс (pm2 reload в cluster-режиме запускает новые воркеры до остановки старых) или запускайте новый контейнер и переключайте proxy после того, как он прошёл health check. Остерегайтесь рассинхрона версий: браузеры со старой открытой страницей могут запрашивать старые JavaScript-чанки, поэтому какое-то время держите предыдущие статические ассеты доступными.

bash
#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/app
RELEASE=$APP/releases/$(date +%Y%m%d%H%M%S)
git clone --depth 1 --branch main [email protected]:your_user/app.git "$RELEASE"
cd "$RELEASE"
rm -rf storage && ln -s $APP/shared/storage storage
ln -s $APP/shared/.env .env
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build
php artisan migrate --force
php artisan optimize
ln -sfn "$RELEASE" $APP/current          # atomic switch
sudo systemctl reload php8.3-fpm         # match your PHP version
php artisan queue:restart

07Миграции базы данных и откаты

Код можно откатить за секунды, данные — нет. Во время любого деплоя старый и новый код какое-то мгновение работают одновременно, поэтому каждая миграция должна работать с обеими версиями. Паттерн expand and contract делает это безопасным. Чтобы переименовать колонку: сначала добавьте новую колонку и пишите в обе (expand), задеплойте, заполните старые строки пачками, переключите чтение на новую колонку и только в одном из следующих релизов удалите старую (contract). Избегайте долгих блокировок больших таблиц; добавляйте индексы конкурентно, если база это поддерживает. Откат — это повторный деплой предыдущей папки релиза или тега образа, поэтому держите несколько последних. Для базы данных лучше катиться вперёд исправляющей миграцией, чем запускать down-миграции в продакшене, и всегда имейте свежий бэкап перед рискованными изменениями.

  • Сначала только добавление: новые колонки nullable или со значениями по умолчанию.
  • Разрушающие изменения (удаление, переименование) — только когда старую структуру больше не использует никакой код.
  • Держите на диске несколько предыдущих релизов для мгновенного отката.

Когда применять

  • Любой проект, где больше одного разработчика или больше одного деплоя в месяц.
  • Везде, где простой стоит денег: магазины, системы бронирования, платёжные сценарии, SaaS.
  • Когда клиенту нужен аудит: что изменилось, когда и почему.
  • И для мобильных приложений: пайплайны могут единообразно собирать, подписывать и загружать сборки в сторы.

Когда не стоит

  • Полноценная canary-инфраструктура для маленького сайта-визитки — хватит простого атомарного деплоя.
  • Релизные ветки GitFlow в веб-приложении, которое деплоится каждый день: они добавляют церемоний, а не безопасности.
  • Автоматический деплой в продакшен без тестов — так вы просто быстрее отгружаете баги.

Альтернативы

  • GitHub Actions

    CI/CD, встроенный в GitHub, огромный маркетплейс action'ов, workflow на YAML.

  • GitLab CI/CD

    Пайплайны, окружения и container registry в одном продукте; можно хостить у себя.

  • Deployer / Envoyer

    Инструменты для PHP, реализующие атомарный деплой через папки релизов для Laravel.

  • Kamal

    Деплоит Docker-контейнеры на обычные серверы с переключением без простоя, оркестратор не нужен.

Лучшие практики

  1. Защитите main: pull request'ы, обязательные зелёные проверки и хотя бы одно ревью.
  2. Держите ветки короткоживущими, а pull request'ы — такими, чтобы их можно было отревьюить за один присест.
  3. Собирайте один раз и деплойте один и тот же артефакт везде; никогда не пересобирайте для продакшена.
  4. Делайте каждую миграцию обратно совместимой с кодом, который сейчас работает.
  5. Держите наготове несколько последних релизов и потренируйтесь откатываться до того, как это понадобится.
  6. Запускайте health check после каждого деплоя и роняйте пайплайн, если он не прошёл.
  7. Храните секреты в хранилище секретов CI или на сервере, никогда — в Git.

Частые ошибки

  • Делать rebase и force-push ветки, которую коллеги уже стянули.
  • Запускать git pull и composer install прямо в живой папке, оставляя сайт наполовину обновлённым на минуту.
  • Удалять или переименовывать колонку в том же релизе, что и изменение кода.
  • Забыть queue:restart, из-за чего воркеры часами крутят старый код.
  • Огромные pull request'ы, которые одобряют после беглого взгляда, потому что по-настоящему их никто отревьюить не может.

Глубже

08Как Git хранит вашу работу и почему почти ничего не теряется

Git — это база данных с адресацией по содержимому. Каждая версия файла хранится как blob, каждый каталог — как tree, перечисляющий blob'ы и другие tree, а каждый коммит — как объект, который указывает на один корневой tree, родительские коммиты, автора и сообщение. Имя каждого объекта — хэш его содержимого, поэтому одинаковые файлы хранятся один раз, а любая подмена меняет хэш. Ветки и теги — это ref'ы: маленькие файлы с одним хэшем коммита. HEAD указывает на ref, на котором вы сейчас находитесь. Поскольку коммиты никогда не редактируются, а только создаются новые, rebase или reset не уничтожают старые коммиты — они просто теряют свою метку. Reflog записывает каждое положение HEAD и каждой ветки, обычно примерно за 90 дней, так что после неудачного rebase или reset можно найти старый хэш и снова направить на него ветку. Garbage collection удаляет недостижимые объекты только после этого срока.

bash
git reflog -15                      # where HEAD has been
git branch rescue HEAD@{3}          # recover the state from 3 moves ago
git reset --hard ORIG_HEAD          # undo the last rebase or merge
git fsck --lost-found               # find dangling commits and blobs
git cat-file -p HEAD                # see a commit object: tree, parent, author

09Защита самого пайплайна

CI-система держит ключи от продакшена, поэтому атакуют именно её. Важнее всего четыре меры. Первая — минимальные права для токена джоба: явно объявляйте permissions в начале каждого workflow, по умолчанию только чтение. Вторая — полностью откажитесь от долгоживущих облачных ключей: GitHub Actions, GitLab и другие умеют выдавать короткоживущий OIDC-токен, который AWS, Google Cloud или Azure обменивают на временные учётные данные, ограниченные одним репозиторием и веткой. Третья — фиксируйте сторонние actions на полный SHA коммита, а не на тег, потому что тег может передвинуть любой, кто контролирует тот репозиторий. Четвёртая — осторожнее с workflow, которые запускаются pull request'ами из форков: pull_request_target выполняется с секретами базового репозитория, поэтому он никогда не должен делать checkout и запускать недоверенный код. Деплой в продакшен должен идти через защищённое окружение с обязательными ревьюерами, чтобы скомпрометированная ветка не могла задеплоить сама.

yaml
permissions:
  contents: read
  id-token: write          # allow OIDC token for cloud login
jobs:
  deploy:
    environment: production  # required reviewers + env-scoped secrets
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::111122223333:role/deploy-web
          aws-region: eu-central-1

10Собирать один раз, продвигать по digest и выстраивать деплои в очередь

Зрелый пайплайн собирает артефакт ровно один раз — при merge в main, помечает его SHA коммита и пушит в registry. Staging и продакшен затем деплоят тот же самый артефакт, определяемый неизменяемым digest, так что протестированное байт в байт совпадает с выкаченным. Пересборка для продакшена снова вносит риск: за это время могли поменяться зависимость или базовый образ. Два одновременных деплоя могут оставить сервер наполовину на каждой версии, поэтому выстраивайте их в очередь через concurrency group и не отменяйте уже идущий деплой: прерваться на середине хуже, чем довести до конца. Записывайте каждый деплой с коммитом, временем и автором, чтобы на вопрос «что сейчас крутится в продакшене» был ответ в одну строку. Откат тогда — это просто повторный деплой предыдущего digest.

yaml
concurrency:
  group: deploy-production
  cancel-in-progress: false     # queue, never kill a running deploy
 
# build stage (once):
#   docker build -t registry.example.com/web:$GITHUB_SHA .
#   docker push registry.example.com/web:$GITHUB_SHA
# deploy stages reuse the same tag/digest, no rebuild

11Изменения схемы на больших таблицах без блокировки сайта

Expand and contract сохраняет совместимость кода и схемы, но каждый шаг всё равно может заблокировать таблицу. В PostgreSQL большинство форм ALTER TABLE берут блокировку ACCESS EXCLUSIVE. Даже быстрое изменение должно дождаться окончания любой долгой транзакции, а пока оно ждёт, все новые запросы к этой таблице встают в очередь за ним — и миграция на две миллисекунды может заморозить сайт на минуты. Защищайтесь коротким lock_timeout и повторами. Индексы стройте через CREATE INDEX CONCURRENTLY: он не блокирует запись, но не может выполняться внутри транзакции, поэтому во фреймворках, которые оборачивают миграции в транзакции, для такой миграции это нужно отключить (в Laravel — выставить свойство миграции withinTransaction в false). Добавлять NOT NULL на огромную таблицу безопаснее как CHECK-ограничение с NOT VALID и последующей валидацией. В MySQL предпочитайте ALGORITHM=INSTANT, где он поддерживается, или инструменты вроде gh-ost для больших перестроений.

sql
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN export_status text;   -- metadata-only, fast
 
CREATE INDEX CONCURRENTLY idx_orders_export_status
  ON orders (export_status);
 
ALTER TABLE orders ADD CONSTRAINT orders_export_status_nn
  CHECK (export_status IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_export_status_nn;

Реальные ситуации

01

Секрет закоммитили и запушили

Проблема

Разработчик замечает, что коммит, запушенный час назад, содержит продакшен-ключ API в конфиг-файле. Репозиторий приватный, но несколько человек и CI-система уже его стянули.

Как решить

Сначала ротация, потом уборка. Немедленно отзовите ключ у провайдера и выпустите новый: всё, что было запушено, считается утёкшим, — и проверьте логи провайдера на неожиданное использование. Затем уберите файл из кода и перенесите значение в хранилище секретов. Переписывать историю через git filter-repo для приватного репозитория необязательно, и это вынуждает всех заново клонировать репозиторий, так что делайте это, только если того требует политика. Наконец, не допускайте повторения: включите secret scanning и push protection на хостинге, добавьте pre-commit hook вроде gitleaks и внесите шаблон конфиг-файла в .gitignore.

02

Баг появился где-то в последних 200 коммитах

Проблема

В редком случае суммы заказов считаются неверно. Последний точно корректный релиз был три недели назад, и никто не может сказать, какой из примерно 200 коммитов внёс проблему.

Как решить

Используйте git bisect — он делает бинарный поиск по истории, так что для 200 коммитов нужно всего около восьми проверок. Напишите небольшой скрипт или тест, который воспроизводит баг и завершается с ненулевым кодом при ошибке. Затем выполните git bisect start, git bisect bad HEAD, git bisect good v1.7.0 и git bisect run ./check-totals.sh. Git сам переключается на коммиты, запускает скрипт и сообщает первый плохой коммит. Прочитайте его diff, превратите скрипт в постоянный регрессионный тест, исправьте баг и закончите командой git bisect reset. Bisect лучше всего работает, когда каждый коммит собирается, — ещё одна причина делать маленькие самодостаточные коммиты.

03

Пайплайн идёт 25 минут, и его перестают ждать

Проблема

Разработчики мержат, не дождавшись CI, или перезапускают упавшие джобы, пока те не пройдут. Main краснеет дважды в неделю, и проверкам больше никто не доверяет.

Как решить

Сначала измерьте каждый шаг — обычно доминируют два-три. Кэшируйте загрузку зависимостей с ключом по lockfile и кэшируйте результаты сборки, где инструмент это поддерживает. Разделите независимую работу на параллельные джобы: линт и проверка типов, юнит-тесты и сборка могут идти бок о бок, а большие наборы тестов можно шардировать на несколько раннеров. В монорепозитории пропускайте нерелевантные джобы через фильтры по путям. Нестабильные тесты отправьте в отдельный неблокирующий джоб с владельцем и дедлайном вместо того, чтобы разрешать перезапуски. Затем сделайте проверки обязательными в branch protection или добавьте merge queue, чтобы ничего не попадало в main без зелёного прогона. Цель меньше десяти минут реалистична для большинства веб-приложений.

Шпаргалка

Восстановить потерянный коммит или ветку
git reflog then git branch rescue <hash>
Безопасно отменить влитый pull request
git revert -m 1 <merge-commit-hash>
Найти коммит, который что-то сломал
git bisect start && git bisect bad && git bisect good v1.7.0 && git bisect run ./test.sh
Найти, когда строку добавили или удалили
git log -S "calculateTotal" --oneline
Поправить более ранний коммит до ревью
git commit --fixup <hash> && git rebase -i --autosquash origin/main
Работать над хотфиксом без stash
git worktree add ../app-hotfix origin/main
Force-push, только если никто больше не пушил
git push --force-with-lease
Выстроить деплои в очередь в GitHub Actions
concurrency: { group: deploy-production, cancel-in-progress: false }
Дать CI войти в облако без ключей
permissions: { id-token: write, contents: read }
Индекс в Postgres без блокировки записи
CREATE INDEX CONCURRENTLY idx_name ON orders (column);

В моих реальных проектах

  • Примерно 7 100 коммитов в 19 продакшен-репозиториях — около половины всех коммитов в репозиториях команды.
  • У приложений на React Native есть задокументированный процесс релиза, а force-update через Remote Config служит страховкой, когда старую версию нужно вывести из оборота.
  • Офлайн-хаб филиала в POS SaaS использует версионированные миграции SQLite, закреплённые по контрольной сумме, так что изменённая миграция не может тихо выполниться.
  • Изменения проходят через workflow агентов «планировщик — исполнитель — ревьюер» с хуками проверки типов после правок, прежде чем что-то будет влито.

Вопросы и ответы

Q1Чем merge отличается от rebase и когда что использовать?

Merge объединяет две ветки merge-коммитом с двумя родителями; история остаётся ровно такой, какой была, включая момент расхождения веток. Rebase берёт ваши коммиты и переигрывает их поверх другой базы, давая линейную историю, но создаёт новые коммиты с новыми хэшами. Я делаю rebase своей feature-ветки на свежий main перед открытием или обновлением pull request'а: так diff остаётся чистым, а конфликты разрешает тот, кто знает изменение. Я никогда не делаю rebase коммитов, которые другие уже стянули. Для вливания в main подойдёт merge или squash-merge — команде просто нужно выбрать одну договорённость.

Q2Trunk-based разработка или GitFlow?

Для веб-приложения, которое часто деплоится, — trunk-based. Короткоживущие ветки, влитые за день-два, избавляют от больших конфликтов, держат main всегда готовым к релизу и подходят для continuous deployment. Незаконченная работа прячется за feature flag'ами, а не живёт в ветке неделями. GitFlow по-прежнему имеет смысл, когда релизы версионированы и идут по расписанию — например, мобильные приложения, проходящие ревью в сторах, десктопный софт или продукты, где параллельно поддерживается несколько версий. Даже тогда я держу feature-ветки короткими. Стратегия важна меньше, чем дисциплина: маленькие изменения, защищённый main, обязательные проверки и ревью.

Q3Чем отличаются continuous integration, delivery и deployment?

Continuous integration означает, что каждое изменение часто вливается, автоматически собирается и тестируется, так что проблемы интеграции всплывают за минуты. Continuous delivery идёт дальше: каждое изменение, прошедшее пайплайн, даёт релиз-кандидат, который можно в любой момент задеплоить в продакшен, обычно по кнопке ручного подтверждения. Continuous deployment убирает эту кнопку, и каждое зелёное изменение уходит в продакшен автоматически. Для этого нужны сильные тесты, хороший мониторинг и быстрый откат, потому что ни один человек не проверяет каждый релиз. Многие команды делают continuous deployment на staging и continuous delivery в продакшен — разумная золотая середина.

Q4Как запускать миграции базы данных без простоя?

Я исхожу из того, что старый и новый код какое-то время будут работать одновременно, поэтому каждая миграция должна быть совместима с обоими. Это значит expand and contract. Expand: добавить новые таблицы или nullable-колонки, задеплоить код, который пишет в обе структуры, заполнить существующие строки пачками. Contract: когда старую структуру уже никто не читает, удалить её в одном из следующих релизов. Я избегаю операций, которые надолго блокируют большие таблицы, добавляю индексы конкурентно, где это возможно, и запускаю тяжёлые изменения данных фоновыми задачами, а не внутри деплоя. Перед рискованными миграциями я убеждаюсь, что есть свежий бэкап, и в продакшене предпочитаю исправлять вперёд, а не запускать down-миграции.

Q5Blue-green или canary деплой?

Blue-green держит два одинаковых окружения: вы деплоите в простаивающее, проверяете его и затем переключаете весь трафик разом. Откат — это просто обратное переключение, очень быстрое и понятное, но ненадолго нужна двойная мощность, и все пользователи переезжают одновременно. Canary направляет небольшой процент трафика на новую версию, сравнивает уровень ошибок и задержки со старой и шаг за шагом увеличивает долю. Он ограничивает радиус поражения неудачного релиза, но, чтобы он окупался, нужны хорошие метрики, разделение трафика и автоматизация. Маленьким командам обычно хватает большей части пользы от атомарных переключений в стиле blue-green.

Q6Деплой сломал продакшен. Что вы делаете?

Сначала восстанавливаю сервис, потом разбираюсь. Если вероятная причина — деплой, сразу откатываюсь на предыдущий релиз или тег образа; именно поэтому я держу наготове несколько релизов. Если миграция уже изменила данные, откаченный код всё равно должен работать с новой схемой — это гарантирует expand and contract. Если откат невозможен, выкатываю небольшое исправление вперёд. Параллельно сообщаю статус заинтересованным людям. Потом пишу короткий blameless postmortem: хронология, первопричина, почему тесты и мониторинг этого не поймали, и одно-два конкретных действия — новый тест, алерт или проверка при деплое.

Вопросы уровня senior

S1Как бы вы спроектировали CI/CD для монорепозитория с несколькими приложениями?

Цель — чтобы каждое изменение запускало только затронутую им работу, а изменения общего кода всё равно тестировали всё, что от него зависит. Инструмент сборки, понимающий граф зависимостей, например Turborepo, Nx или Bazel, умеет вычислять затронутые проекты и кэшировать результаты задач локально и удалённо, так что неизменённые пакеты не пересобираются. CI затем запускает линт, тесты и сборку для каждого затронутого приложения в параллельных джобах, а у каждого приложения свой пайплайн деплоя, версия и откат. Фильтры по путям — более простой старт, но они пропускают транзитивные зависимости. Компромиссы: монорепозиторий делает сквозные изменения атомарными и держит инструменты единообразными, но требует вложений в кэширование и правила владения, обычно CODEOWNERS по каталогам. Polyrepo упрощают пайплайны, но превращают обновление общих библиотек в рутину «выпусти и подними версию» во многих репозиториях.

S2Feature flags или долгоживущие ветки для больших фич?

В большинстве случаев флаги. Долгоживущая ветка уходит всё дальше от main, поэтому финальный merge получается большим, рискованным и тяжёлым для ревью, а фича тестируется на старой версии всего остального. С флагами код вливается маленькими частями за выключенным в продакшене переключателем, интеграция идёт непрерывно, и можно включить фичу для сотрудников, затем для одного процента пользователей, затем для всех — и мгновенно выключить без деплоя. Издержки реальны: оба пути кода должны работать и тестироваться, флаги добавляют условную сложность, а забытые флаги превращаются в технический долг. Снижайте это соглашением об именовании, владельцем и сроком жизни для каждого флага, а также задачей на очистку, которая заводится в тот день, когда флаг доходит до ста процентов.

S3Миграция необратима. Как всё равно сделать релиз безопасным?

Примите, что откат — это откат кода, а не данных, и проектируйте так, чтобы этого всегда хватало. Разбейте изменение так, чтобы рискованная разрушающая часть шла последней и отдельно: сначала добавляющая миграция, которую старый код игнорирует, затем новый код, который её использует, и только через релиз-два — разрушающий шаг, когда метрики покажут, что старую структуру никто не читает. Перед этим финальным шагом сделайте и проверьте свежий бэкап или убедитесь, что point-in-time recovery его покрывает, и прогоните миграцию на копии размером с продакшен, чтобы измерить длительность и блокировки. Большие перезаписи данных запускайте идемпотентными фоновыми задачами пачками, чтобы их можно было ставить на паузу и возобновлять. Если что-то всё же пойдёт не так, план — чинить вперёд новой миграцией, а бэкап нужен на случай катастрофы, а не для рутинного отката.

Глоссарий

Ref
Именованный указатель на коммит, например ветка или тег. Перемещение ветки — это просто перезапись её ref.
HEAD
Ref того, что у вас сейчас выбрано (checkout); обычно указывает на ветку, а она — на коммит.
Reflog
Локальный журнал всех положений HEAD и веток; с его помощью восстанавливают коммиты после reset или rebase.
Fast-forward
Merge, при котором целевая ветка просто сдвигается вперёд на новый коммит, потому что история не разошлась.
Squash merge
Объединение всех коммитов pull request'а в один коммит в main — история получается короткой и читаемой.
Merge queue
Система, которая перед merge тестирует каждый pull request поверх свежего main и изменений, стоящих в очереди.
OIDC-токен
Короткоживущий подписанный токен идентичности, который CI выдаёт джобу, а облако обменивает на временные учётные данные.
Артефакт
Результат сборки пайплайна, например образ контейнера или архив, который без изменений деплоится во все окружения.
Feature flag
Переключатель во время работы, который включает или выключает ветки кода без деплоя, отделяя релиз от деплоя.
Version skew
Старая и новая версии кода или клиент и сервер работают одновременно и должны уживаться друг с другом.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.