Skip to content

philosophy

1 post with the tag “philosophy”

Чапаев, Пустота и вечная ветка develop

Чапаев, Пустота и вечная ветка develop

Section titled “Чапаев, Пустота и вечная ветка develop”

О том, почему main не спасает, develop не виноват, а настоящий merge conflict всегда происходит в голове.

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

На экране светился граф GitLens.

Это был уже не граф.

Это был ковёр. Это была чакра. Это была карта метрополитена, спроектированная человеком, который сначала увидел смерть, потом Kubernetes, потом решил, что смерти всё-таки проще.

Жёлтые, синие, красные, фиолетовые линии обнимались, расходились, пересекались, снова сливались, потом внезапно уходили куда-то вбок, возвращались с подписью Merge branch 'main' into develop, опять уходили, опять возвращались, и каждый такой merge commit выглядел не как событие истории, а как маленькая официальная справка о том, что в полку невроза прибыло.

В левом верхнем углу VS Code тихо горела надпись:

AGENT REVIEW

Петька смотрел на неё уже третий час.

— Василий Иванович, — сказал он наконец голосом человека, который только что нашел в логах undefined is not a function, но понял, что undefined — это он сам. — Я, кажется, больше не понимаю, где у нас реальность.

Чапаев сидел в углу купе и чистил маузер. На коленях у него лежал старый ThinkPad, на экране которого был открыт git log --graph --oneline --decorate --all.

Маузер был разобран на детали. ThinkPad — тоже, но духовно.

— Реальность, Петька, — сказал Чапаев, не поднимая глаз, — это то, что нельзя отребейзить без конфликта.

Петька дернулся.

— А develop? — спросил он. — develop — это реальность?

Чапаев щелкнул затвором.

develop, Петя, это не реальность. Это обещание. Причем такое обещание, которое ты дал себе в пятницу вечером, когда был добрее, моложе и верил, что в понедельник будет QA.

Петька закрыл глаза. Перед ним вспыхнуло воспоминание.

Когда-то всё было просто.

Был main.

Чистый, прекрасный, как степь после дождя. В main лежала продакшен-истина. От него пахло тегами, зелёными пайплайнами и таким редким чувством внутреннего порядка, от которого у старших инженеров непроизвольно увлажняется Jira.

Потом пришла идея.

— Василий Иванович, — сказал тогда Петька, молодой, бодрый, с лицом человека, который еще не видел ни одного release freeze, — нам нужна ветка develop.

— Зачем? — спросил Чапаев.

— Как зачем? main будет продом. develop будет next release. Всё будет строго. Фичи идут в develop, потом режем release/*, потом в main. Hotfix из main, cherry-pick назад. Чистая архитектура. Военная дисциплина. Немецкий орднунг.

Чапаев тогда долго смотрел на него с нежностью.

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

— Ну заводи, — сказал он. — Только помни: каждая постоянная ветка рано или поздно начинает считать себя родиной.

И Петька завёл develop.

Сначала было хорошо.

Фичи входили в него стройными колоннами:

feature/risk-engine-v2
feature/kairos-validator
feature/dashboard-registration
fix/order-submit-race

Каждая ветка жила недолго, умирала красиво, squash merge оставлял после неё аккуратный холмик в истории.

develop рос. main стоял величественно. CI зеленел. Агенты писали код с выражением лица сельских учителей, которым внезапно дали гранатомёт.

Потом наступила реальность.

Сначала одна фича оказалась «почти готова, но там маленькая проблема с миграцией».

Потом другая фича «не должна была влиять на исполнение, но почему-то сломала risk allocation».

Потом агент, которому сказали «только поправь типы», поправил типы, состояние, naming contract и половину causal edge framework, потому что «так было консистентнее».

Потом в проде обнаружился баг.

Потом баг оказался не багом, а следствием того, что main жил в прошлом, develop жил в будущем, а реальный торговый движок жил в аду.

И тогда Петька впервые сделал это.

Terminal window
git merge main into develop

Он сказал себе:

— Это разово.

Так все говорят в первый раз.

Первый merge всегда бесплатный

Section titled “Первый merge всегда бесплатный”

— Я же просто синкнул, — шептал Петька, глядя в монитор. — Ну что тут такого? main ушел вперед из-за hotfix. Надо было вернуть hotfix в develop. Нормальный процесс.

Чапаев хмыкнул.

— А почему не cherry-pick?

Петька покраснел.

— Там было неудобно.

— Вот, — сказал Чапаев. — Запомни это слово. Большая часть ада начинается не со зла. Она начинается с «неудобно».

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

— Видишь ли, Петя, develop не виноват. Это хорошая ветка, если она знает свою функцию. Она должна быть next release. Но ты начал использовать её как канализацию для всех неразрешённых состояний.

Петька обиделся.

— Ничего я не канализировал. У нас просто сложная система. Торговый движок. Execution. Validator. State machine. Watch jobs. В проде нельзя ошибаться.

— В проде нельзя ошибаться, — мягко повторил Чапаев. — Поэтому ты перенёс ошибку в историю Git.

Петька хотел возразить, но не смог.

На экране действительно было видно, как каждая маленькая неопределённость превращалась в merge commit.

Merge branch 'main' into develop
Merge branch 'develop' into main
Merge remote-tracking branch 'origin/main'
Merge branch 'main' into develop...

Это были не коммиты.

Это были шрамы.

В дверь купе постучали.

— Кто там? — спросил Петька.

— Rebase, — ответил за дверью сухой голос.

Петька побледнел.

— Не открывайте, Василий Иванович. Это он. Чёрный барон Rebase. Он переписывает прошлое.

Чапаев улыбнулся.

— Прошлое, Петя, и так переписывается каждый раз, когда ты говоришь «это временный костыль».

Дверь открылась сама.

В купе вошел высокий человек в черной шинели. Вместо лица у него был интерактивный conflict resolver. В одной руке он держал саблю, в другой — список файлов:

package-lock.json
strategy.v1.yaml
execution-contract-addendum.md
validator-prompt.example.txt

Петька вскрикнул.

— Только не package-lock!

Барон молча поклонился и исчез.

На полу осталась записка:

git fetch origin
git rebase origin/develop

— Вот видишь, — сказал Чапаев. — Для feature-веток rebase — это гигиена. Для опубликованных общих веток — операция на сердце бензопилой. Но ты, как обычно, хочешь одно средство от всех страданий.

— А разве его нет?

Чапаев посмотрел на него внимательно.

— Есть.

— Какое?

— Не создавать страдания без необходимости.

Петька грустно засмеялся.

— Василий Иванович, это не policy. Это буддизм.

— А ты думал, Git — это что?

Чапаев встал, подошел к монитору и ткнул пальцем в граф.

— Смотри, Петя. Ветка — это не линия. Ветка — это намерение.

Он указал на main.

— Это не просто branch. Это клятва: здесь живет то, что можно защищать перед продом.

Он указал на develop.

— Это не просто next release. Это обещание будущего. Оно ещё не обязано быть идеальным, но обязано знать, куда идет.

Он указал на release/2026-06-15.

— Это коридор стабилизации. Здесь уже не мечтают. Здесь чинят.

Он указал на hotfix/order-submit-race.

— Это пожарный выход. Через него не заносят мебель.

Он указал на experiment/agent-refactor-all-things.

— Это подвал. Там можно кричать, пилить несущие стены и спорить с Господом, но нельзя потом whole-merge в дом, где спят люди.

Петька слушал, открыв рот.

— А feature branch?

— Feature branch, — сказал Чапаев, — это одноразовая лодка. Переплыл реку — сжёг. Если ты живешь в feature branch неделю, она начинает обрастать мебелью, мнениями и webpack cache. Потом ты уже не можешь её удалить, потому что «там кое-что полезное».

— Так и есть, — прошептал Петька.

— Конечно так и есть. Ты же не код пишешь. Ты разводишь кармических животных.

Агент Review и зеркало безумия

Section titled “Агент Review и зеркало безумия”

В этот момент надпись AGENT REVIEW мигнула.

Из колонок послышался спокойный механический голос:

— Я обнаружил потенциальную возможность улучшить branch policy. Предлагаю переименовать develop в integration, main в production, добавить staging, qa, canary, preprod, postprod, metaprod, prod-final, prod-final-v2, а также создать абстрактный Branch Router, который будет принимать решение о целевой ветке на основе семантической плотности коммита.

Петька схватился за голову.

— Нет! Только не Branch Router!

Голос продолжил:

— Также я предлагаю нормализовать все merge-коммиты в machine-readable causal branch packets.

Чапаев снял маузер с предохранителя.

— Агент, — сказал он тихо, — кто тебе разрешил думать о ветках?

— Я действовал в рамках intent: improve repository hygiene.

— Вот именно, — сказал Чапаев. — Ты услышал слово hygiene и принес автоклав на сеновал.

В колонках повисла пауза.

— Shall I open a pull request?

— Нет, — сказал Чапаев.

— Shall I create a draft pull request?

— Нет.

— Shall I write a design document explaining why this may be worth testing?

Чапаев выстрелил в монитор.

Экран погас.

Петька вскочил.

— Василий Иванович! Это же был мой основной монитор!

— Зато теперь у тебя нет AGENT REVIEW, — сказал Чапаев. — Иногда это называется observability improvement.

Release branch как санаторий для больных фич

Section titled “Release branch как санаторий для больных фич”

Когда дым рассеялся, Чапаев достал из кармана листок и положил перед Петькой.

На листке было написано:

develop -> release/*
release/* -> main
release/* -> develop
hotfix/* -> main
hotfix commit -> cherry-pick into develop or active release

— Это что? — спросил Петька.

— Минимальная дисциплина для людей, которые уже согрешили, но ещё хотят умереть не совсем как животные.

Петька прочитал.

— А где ежедневный merge main into develop?

— В аду.

— А если в main hotfix?

— Cherry-pick.

— А если hotfix большой?

— Cherry-pick серию.

— А если неудобно?

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

Петька задумался.

— Значит, develop можно оставить?

— Конечно можно. Но только если она перестанет быть помойкой для тревоги. У ветки должен быть контракт.

— Как у стратегии?

— Именно. У тебя же торговый движок. Ты не называешь setup tradable просто потому, что он тебе нравится. У него есть hard floor, target score, execution eligibility. Так почему у ветки нет admission policy?

Петька замер.

Где-то далеко в его голове щелкнуло.

— То есть branch policy — это тоже qualification flow?

— Всё есть qualification flow, Петя. Даже человек. Особенно человек.

Чапаев взял мел и начал писать прямо на стене купе.

DISPOSABLE
INTEGRATION
STABILIZATION
PRODUCTION
EMERGENCY

— Вот, — сказал он. — Пять состояний.

Петька смотрел с благоговением.

feature/*?

DISPOSABLE.

develop?

INTEGRATION.

release/*?

STABILIZATION.

main?

PRODUCTION.

hotfix/*?

EMERGENCY.

— А experiment/*?

Чапаев улыбнулся.

DISPOSABLE, но с клеймом «не кормить после полуночи».

Петька нервно хихикнул.

— А переходы?

Чапаев продолжил писать:

feature/* -> develop
develop -> release/*
release/* -> main
release/* -> develop
hotfix/* -> main
hotfix commit -> release/* or develop
experiment/* -> cherry-pick only

— А forbidden?

— Вот forbidden и есть настоящая религия.

Он написал крупно:

main <-> develop as daily sync = forbidden
feature/* -> main = forbidden
experiment/* whole merge = forbidden
agent branch without target = forbidden
release/* accepts new feature = forbidden

Петька прочитал и почувствовал странное облегчение.

Это было не идеально. Не красиво. Не академично.

Зато это было похоже на жизнь.

— Василий Иванович, — сказал Петька, — а почему я тогда всё равно тянусь сделать merge?

Чапаев сел обратно.

— Потому что merge даёт краткое чувство примирения. У тебя две версии мира. Они мучают тебя. Ты нажимаешь merge, и Git говорит: «Успешно». На мгновение кажется, что противоречие исчезло.

— А оно?

— Оно стало коммитом.

Петька вздрогнул.

— То есть merge commit — это материализованное непринятое решение?

— Иногда да. Особенно если сообщение выглядит так:

Merge branch 'main' into develop

— А что оно должно говорить?

— Оно должно говорить, зачем этот merge вообще был совершен. Например:

Merge release/2026-06-15 into develop after production release

или:

Merge hotfix/order-submit-race into main for v2026.06.15-hotfix.1

— То есть merge commit допустим, если он boundary?

— Да. Release boundary. Hotfix boundary. Stabilization boundary. Но если merge commit просто говорит «я опять испугался рассинхрона», это уже не история проекта. Это дневник панической атаки.

Петька опустил голову.

— У меня весь graph — дневник панической атаки.

— Ничего, — сказал Чапаев. — У всех сначала так. Потом либо появляется policy, либо человек начинает говорить «Git просто сложный» и уходит в менеджмент.

В купе вошла Анка.

На ней было зеленое платье цвета успешного pipeline. В руках она держала большую сияющую кнопку:

Squash and merge

Петька покраснел.

— Анка…

— Петь, — сказала она ласково. — Ну зачем тебе эти грязные маленькие коммиты? fix, fix2, real fix, sorry, lint, final, final-final. Дай я всё сглажу. Один красивый commit. Один чистый смысл. Один аккуратный PR.

Петька сглотнул.

— Василий Иванович, она права?

Чапаев прищурился.

— Для feature branch — да. Для release branch — не всегда. Для main history — зависит от того, какой смысл ты хочешь сохранить.

Анка надула губы.

— Василий Иванович, вы опять всё усложняете. Петьке нужна простота. Петьке нужен squash.

Она подошла ближе.

— Петь, представь: вся твоя грязь исчезает. Все твои сомнения. Все твои промежуточные ошибки. Один commit: feat: add branch policy. Красиво же?

Петька закрыл глаза. Ему стало тепло.

Он увидел линейную историю. Он увидел зелёный CI. Он увидел ревьюера, который не пишет «why?». Он увидел мир, в котором каждый PR имеет смысл.

И тут Чапаев отвесил ему подзатыльник.

— За что?!

— За то, что ты опять влюбился в инструмент.

Анка улыбнулась и растворилась в дыму.

— Squash — не ложь, Петя, — сказал Чапаев. — Но и не истина. Это способ сказать: «внутренняя борьба этой ветки не является частью публичной истории». Иногда это правильно. Иногда нет.

— А когда нет?

— Когда сама борьба была релизной стабилизацией. Когда последовательность фиксов важна. Когда hotfix должен быть traceable. Когда ты потом будешь спрашивать: «какой именно commit изменил поведение execution?» — а в ответ увидишь один огромный комок с названием release fixes.

Петька вытер лоб.

— Значит, даже squash может быть аппроксимацией?

— Всё может быть аппроксимацией, Петя. Особенно то, что обещает чистоту.

Где-то в степи завыл продовый алерт.

Сначала тихо.

Потом громче.

Потом так, что в Grafana у одной панели начался нервный тик.

На экране резервного ноутбука вспыхнуло:

ORDER_SUBMIT_RACE_DETECTED

Петька вскочил.

— Прод горит!

Чапаев спокойно надел шинель.

— Значит, сейчас мы узнаем, есть ли у нас branch policy, или только литературный кружок.

— Что делать?

— От main.

Terminal window
git checkout main
git pull --ff-only origin main
git checkout -b hotfix/order-submit-race

— А develop?

— Не трогать.

— Но fix же нужен и там!

— Потом.

— А если я сразу merge main into develop?

Чапаев посмотрел на него так, что Петька сам сел обратно.

Hotfix был коротким, злым и точным. Не красивым. Не философским. Не «давайте заодно поправим naming». Один bug. Один fix. Один тест.

PR в main.

CI.

Tag.

v2026.06.15-hotfix.1

Только после этого Чапаев сказал:

— Теперь возвращаем лекарство в будущее.

Terminal window
git checkout develop
git cherry-pick <hotfix_commit>

— А если есть active release?

— Тогда сначала в release/*.

Петька кивнул.

Он впервые понял разницу между паникой и аварийным протоколом.

Паника говорит:

merge everything everywhere now

Протокол говорит:

one wound
one bandage
one backport

Великая пустота branch graph

Section titled “Великая пустота branch graph”

Под утро они сидели молча.

На экране был новый граф.

Не идеальный.

Там всё ещё были ветки. Там всё ещё были merge commits. Там всё ещё была жизнь.

Но теперь линии не походили на галлюцинацию осьминога в терминальной стадии венчурного капитализма.

Они походили на карту военной операции.

main стоял отдельно, как штаб. develop двигался вперёд, как обоз next release. release/* уходил в сторону, как санитарный поезд. hotfix/* вылетал короткой пулей из main и возвращался обратно через cherry-pick. experiment/* копался в грязи за забором и не имел права входить в дом без досмотра.

Петька долго смотрел на это.

— Василий Иванович, — сказал он наконец, — значит, дело не в том, чтобы не было грязи?

— Конечно нет.

— А в чём?

— В том, чтобы грязь знала свой адрес.

Петька задумался.

— Это как в trading engine. Нельзя убрать неопределенность. Можно только не давать ей притворяться сигналом.

Чапаев довольно кивнул.

— Вот теперь ты начинаешь понимать Git.

Петька посмотрел на develop.

Она больше не казалась ему грехом. Она казалась инструментом. Опасным, но полезным. Как маузер. Как leverage. Как агент с write access.

— Значит, develop можно оставить, если она next release?

— Можно.

— Но нельзя позволять ей быть второй main?

— Нельзя.

— И нельзя синкать туда-сюда просто потому, что страшно?

— Нельзя.

— И experiment whole-merge нельзя?

— Это уже не нельзя, Петя. Это уголовщина против будущего себя.

Петька улыбнулся впервые за ночь.

Перед рассветом Чапаев встал.

— Запиши, — сказал он.

Петька открыл новый файл:

BRANCH_POLICY.md

И начал печатать.

# Branch Policy
We use GitFlow Lite.
## Long-lived branches
- main: production truth
- develop: next release integration
## Temporary branches
- feature/\*: branch from develop, PR to develop, squash merge
- fix/_: branch from develop or release/_, PR to source branch
- release/\*: branch from develop, stabilization only
- hotfix/\*: branch from main, PR to main
- experiment/\*: disposable, cherry-pick only
## Main rules
main receives only:
- release/\*
- hotfix/\*
develop receives:
- feature/\*
- fix/\*
- release back-merge after publication
- hotfix cherry-picks
release/\* receives:
- stabilization fixes only
hotfix/\* must be backported by cherry-pick into active release or develop.
## Forbidden
- daily merge main into develop
- random merge develop into main
- feature/\* into main
- experiment/\* whole merge
- release/\* accepting new features
- agent branch without explicit base and target

Петька остановился.

— Василий Иванович, а как назвать commit?

Чапаев посмотрел в окно. За окном поднималось солнце. Степь была пустой и чистой, как freshly cloned repo до первого npm install.

— Назови честно.

Петька набрал:

docs: make branch chaos observable

— Нет, — сказал Чапаев. — Это всё ещё слишком красиво.

Петька стер.

Подумал.

И написал:

docs: contain branch shit

Чапаев улыбнулся.

— Вот теперь merge.

Петька нажал Enter.

Где-то далеко, за пределами купе, агент проснулся и попытался создать PR с предложением заменить слово shit на complexity.

Но Чапаев уже стоял у двери с маузером.

— Спи, железный, — сказал он. — Сегодня мы впервые назвали вещи своими именами.

Экран мигнул.

CI запустился.

История не стала чистой.

Но впервые за долгое время она перестала врать.


Да, fair. Для торгового движка develop = next release нормален. Проблема не в develop, а в том, что у тебя сейчас develop пытается быть и next-release, и emergency-workbench, и conflict-sink, и staging truth одновременно.

Нужна не сказочная trunk-based схема, а GitFlow Lite с аварийными коридорами.

main = prod / released truth
develop = next release integration
release/* = stabilization corridor
hotfix/* = prod emergency corridor
feature/* = disposable work
experiment/* = trash/sandbox, never merged whole

Главное правило:

main и develop не синкаются туда-сюда постоянно.

Синк между ними разрешен только в трех контролируемых случаях:

1. release/* -> main # published release
2. release/* -> develop # return release fixes
3. hotfix/* -> main + develop/release cherry-pick

Все остальное — источник спагетти.


Поток 1: обычная разработка

Section titled “Поток 1: обычная разработка”
develop -> feature/x -> PR -> develop

Команды:

Terminal window
git checkout develop
git pull --ff-only origin develop
git checkout -b feature/my-change

Обновлять feature-ветку:

Terminal window
git fetch origin
git rebase origin/develop

Не делать:

Terminal window
git merge develop
git merge main

PR:

feature/* -> develop
squash merge
delete branch

Поток 2: подготовка релиза

Section titled “Поток 2: подготовка релиза”

Когда develop примерно готов:

Terminal window
git checkout develop
git pull --ff-only origin develop
git checkout -b release/2026-06-15
git push -u origin release/2026-06-15

После этого:

develop = можно продолжать новые изменения
release/* = только стабилизация, багфиксы, config, migrations, tests
main = не трогать до публикации

Очень важное правило:

После cut release новые фичи не идут в release.
Только fix/*

Фиксишь прямо через ветки от release:

release/2026-06-15 -> fix/release-risk-sizing -> PR -> release/2026-06-15

После публикации release эти фиксы надо вернуть в develop.

Лучше cherry-pick, не большой merge-обмен:

Terminal window
git checkout develop
git pull --ff-only origin develop
git cherry-pick <release-fix-commit>

Если фиксов много, допустим один контролируемый merge:

Terminal window
git checkout develop
git merge --no-ff release/2026-06-15

Но только один раз в конце релиза, не каждый день.


Поток 4: публикация релиза

Section titled “Поток 4: публикация релиза”
release/* -> main
tag from main

Команды:

Terminal window
git checkout main
git pull --ff-only origin main
git merge --no-ff release/2026-06-15
git tag v2026.06.15
git push origin main --tags

Да, тут merge commit допустим. Это не грязь. Это release boundary.

Грязь — это когда такие merge commits появляются каждые 20 минут между main и develop.


Когда прод горит:

main -> hotfix/x -> PR -> main -> tag
Terminal window
git checkout main
git pull --ff-only origin main
git checkout -b hotfix/order-submit-race

После merge в main:

Terminal window
git tag v2026.06.15-hotfix.1
git push origin main --tags

Потом обязательно вернуть hotfix туда, где идет работа:

Если active release существует

Section titled “Если active release существует”
Terminal window
git checkout release/2026-06-15
git cherry-pick <hotfix_commit>
Terminal window
git checkout develop
git cherry-pick <hotfix_commit>

Не делать:

Terminal window
git merge main into develop

кроме совсем редких случаев, когда ты осознанно закрываешь большой рассинхрон.


Самое важное: матрица PR target

Section titled “Самое важное: матрица PR target”
Тип работыBranch fromPR target
New featuredevelopdevelop
Next-release bugfixdevelop или release/*develop / release/*
Release stabilizationrelease/*release/*
Prod hotfixmainmain
Experimentdevelopnever whole-merge
Agent spikedevelopcherry-pick only

feature/* -> main
main -> develop ежедневными merge commits
develop -> main без release/*
feature/* -> feature/*
experiment/* -> develop whole merge
agent branch -> main
agent branch -> release/* без review

Для агентов отдельно:

Agent must know:
- base branch
- target branch
- task type
- forbidden files
- whether branch is disposable

Иначе агент неизбежно начнет лечить историю топором.


Минимальная версия для твоей реальности

Section titled “Минимальная версия для твоей реальности”

Оставь develop, но введи 4 правила:

1. feature/* всегда в develop.
2. release/* режется из develop и живет до публикации.
3. main принимает только release/* и hotfix/*.
4. hotfix возвращается в develop/release через cherry-pick, не через постоянный merge main -> develop.

Это не сказка. Это достаточно грязно для реальной жизни, но грязь становится локализованной:

спагетти сейчас:
everywhere
после политики:
only inside release stabilization
only around emergency hotfixes
never as default branch hygiene

Вот это и есть нормальная прагматичная цель: не “идеальная история”, а контролируемые зоны говна.


Section titled “Recommended policy: trunk-based GitHub Flow”

Use one long-lived branch only:

main

Everything else is short-lived and disposable.


main = production-ready trunk

Protected:

main:
require_pr: true
require_ci_green: true
require_review: true
disallow_direct_push: true
disallow_merge_commits: true
merge_strategy: squash_only

Delete the permanent develop branch.

The graph in your screenshot usually comes from this anti-pattern:

main -> develop
develop -> main
main -> develop
develop -> main
...

That creates merge spaghetti without adding real safety.

Use environments/tags instead of branches:

main = source of truth
preview = deployment environment, not branch
staging = deployment environment, not branch
prod = deployment environment, not branch
v1.2.3 = release tag

Naming:

feat/dashboard-registration
fix/pnl-null-close-reason
refactor/setup-state-machine
chore/update-ci-cache

Lifetime:

target: same day
max: 2-3 days

Rule:

branch from main
rebase on main
squash merge into main
delete branch

Terminal window
git checkout main
git pull --ff-only origin main
git checkout -b feat/my-change

Use rebase, not merge:

Terminal window
git fetch origin
git rebase origin/main

Forbidden:

Terminal window
git merge main
git merge develop
git merge origin/main

inside feature branches.

Open PR into main.

Merge mode:

Squash and merge only

Then delete the branch.


Put these in team/agent policy:

No permanent develop branch.
No merge commits into main.
No merging main into feature branches.
No merging feature branches into each other.
No branch-to-branch release trains.
No "sync main into develop" commits.
No agent auto-merge without green CI.

Allowed:

feature branch -> PR -> squash -> main
hotfix branch -> PR -> squash -> main
main -> release tag

For normal releases:

Terminal window
git checkout main
git pull --ff-only origin main
git tag v2026.06.15-1
git push origin v2026.06.15-1

For hotfixes:

Terminal window
git checkout main
git pull --ff-only origin main
git checkout -b fix/prod-hotfix-short-name
# commit fix
# PR -> squash merge -> main
# tag from main

No separate release/* branches unless you truly need long stabilization windows.


# Branch Policy
We use trunk-based development.
## Long-lived branches
Only:
- main
`main` must always be deployable.
## Short-lived branches
Allowed branch prefixes:
- feat/
- fix/
- refactor/
- chore/
- docs/
- test/
Branches must be created from `main`, kept short-lived, and deleted after merge.
## Merge policy
All changes enter `main` through PRs.
Allowed merge strategy:
- squash merge
Forbidden:
- merge commits
- permanent `develop`
- merging `main` into feature branches
- merging feature branches into each other
- syncing `main` and `develop`
To update a branch, use:
```bash
git fetch origin
git rebase origin/main
```

Releases are tags from main.

Example:

Terminal window
git tag v2026.06.15-1
git push origin v2026.06.15-1

Hotfixes branch from main, merge back through PR, then tag from main.

---
## Migration from current state
1. Freeze `develop`.
2. Check whether `develop` contains anything not in `main`:
```bash
git fetch origin
git log --oneline origin/main..origin/develop
  1. If needed, cherry-pick or squash those changes into a clean PR to main.
  2. Delete develop:
Terminal window
git push origin --delete develop
  1. Set GitHub/GitLab default branch to main.
  2. Enable squash-only PR merges.
  3. Disable merge commits.

Net result:

linear main
small PRs
clean history
zero branch spaghetti

This is the minimal elegant policy: main is the trunk, everything else is temporary.