Дві команди. Одна кодова база. 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. Менеджмент отримав те саме єдине джерело правди, якого насправді завжди й хотів — не «одну платформу», а «щоб код збігався». Дзеркало трималося місяцями. Не тому що ідеальне. А тому що обробка помилок — так.

Висновок

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