Две команды. Одна кодовая база. Git-зеркало реального времени, которое я вообще не собирался делать. Разработчики жили в GitLab. Опсы жили в Bitbucket. Ветки расходились всё сильнее с каждым днём, merge-конфликты копились, и обе стороны тихо винили друг друга. Менеджмент на каждом стендапе задавал один и тот же вопрос: почему у нас ничего не сходится? И все давали один и тот же уставший ответ — просто выберите одну платформу и заставьте всех перейти.

Я собрал real-time синхронизацию GitLab и Bitbucket не потому, что это красиво. Я собрал её, потому что принудительная миграция — это проблема людей, переодетая в техническую, а я инженер, а не политик.

Проблема была не в инструменте

Вот что обычно пропускают все советы в духе «просто consolidate your VCS». Разработчики были правы, что любили GitLab. Опсы были правы, что любили Bitbucket. Оба инструмента делали ровно то, что нужно каждой команде. Трение было не между GitLab и Bitbucket. Оно было между двумя группами людей, одной из которых говорили: пожертвуйте своим рабочим процессом, чтобы в презентации можно было написать «единая платформа».

Такой спор можно выиграть на совещании. Но не на практике. Люди обходят принудительные миграции. Держат теневой репозиторий, пушат в старый remote «ну только один раз», и через шесть недель у тебя два источника правды и очень дорогой урок.

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

Архитектура: двусторонняя синхронизация на каждый push

Ядро — двустороннее Git-зеркало на GitLab CI. Каждый push-event запускает pipeline. И этот pipeline делает всю неблагодарную работу:

  • Синхронизацию с учётом конфликтов, чтобы push с одной стороны не затирал вслепую другую.
  • Маппинг веток по naming convention — не каждой ветке нужно перетекать на другую платформу, и правило именования решает, каким именно.
  • Механизм ретраев с настоящими алертами, потому что синхронизация, которая молча падает в 2 часа ночи, хуже той, что громко падает в полдень.

На бумаге это проект на выходные. На бумаге. Идея правда простая: следи за одним remote, повторяй на другом, повторяй снова. Идея никогда и не была сложной частью.

То, о чём никто не предупреждает

А потом пошло всё, что могло пойти не так, по одному edge case за раз.

  • Force push разносил состояние зеркала. Кто-то переписывает историю в GitLab, зеркало честно пытается сделать fast-forward в Bitbucket, и вот уже две истории разошлись так, что простым повтором их не свести.
  • Auth-токены умирали посреди синхронизации в пятницу вечером. Не в рабочее время, без предупреждения. Токен сдох, синхронизация встала, и никто не замечал до понедельника.
  • Webhook-штормы в день релиза забивали очередь pipeline. Двадцать пушей за две минуты превращались в бэклог sync-джоб, которые наступали друг другу на ноги.
  • Два разработчика запушили один и тот же файл на обе платформы в одну и ту же секунду. Настоящий race condition между двумя remote, которые не знают о существовании друг друга.

Каждый из этих случаев реально произошёл. Каждый потребовал своего собственного фикса. И каждый фикс учил одному и тому же: сложная задача не в синхронизации, а во всех способах, которыми она может молча сломаться, пока все уверены, что всё в порядке. Дрифт не объявляет о себе. Он просто лежит несколько дней, пока кто-нибудь не задеплоит не ту ревизию.

Обработка ошибок — это и есть продукт

Хочу быть честным насчёт того, что реально заставило это работать, потому что дело было не в хитром Git-плюмбинге. Дело было в скучной, оборонительной обработке ошибок.

Сама синхронизация — это где-то треть кода. Остальное — это детект force push и осознанная его обработка вместо того, чтобы делать вид, что его не бывает. Ротация и проверка токенов до джобы, а не после её падения. Rate limiting и дедупликация webhook-событий, чтобы шторм в день релиза схлопывался в одну чистую синхронизацию. Блокировки на уровне отдельного файла, чтобы одновременные пуши выстраивались в очередь, а не гонялись. Ничего впечатляющего на вид. И именно поэтому оно держалось.

Результат

Когда edge case'ы перестали меня удивлять:

  • Задержка синхронизации меньше 30 секунд между платформами.
  • Ноль дрифта — кодовая база оставалась единой, в какой remote ни посмотри.
  • Игра во взаимные обвинения закончилась, потому что винить стало нечего.

Разработчики оставили GitLab. Опсы оставили Bitbucket. Менеджмент получил тот самый единый источник правды, который на самом деле всегда и хотел — не «одну платформу», а «чтобы код совпадал». Зеркало держалось месяцами. Не потому что идеальное. А потому что обработка ошибок — да.

Вывод

Иногда лучшая миграция — это отсутствие миграции, просто очень хорошее зеркало. Инженерное решение пережило все переговорные компромиссы, потому что код не играет в политику. Когда попадаешь в войну инструментов, спроси себя: а это точно проблема инструмента? Часто самый быстрый путь — не заставлять людей меняться, а собрать тот самый скучный слой, который позволит им не меняться.