Branch Strategy
Publica.la's standard for all GitHub-hosted repos: trunk-based development on main. No dev branch. No develop branch. One long-lived integration branch, period.
This is a decision, not a preference. The audit behind it is summarized below so future-you (or a new hire) can tell whether conditions have changed enough to revisit it.
The ruleβ
- Feature branches (
feature/*,fix/*,chore/*, etc.) targetmainvia pull request. mainis always deployable. Merging triggers an automatic staging deploy.- Production deploys are gated by the
productionGitHub Environment (Core Team reviewer). - Preview deploys of a feature branch to staging happen on-demand via the
staging-reviewapproval gate on the PR. See Deploy Approval Pattern. - Short-lived release or integration branches are permitted by exception (see Exceptions); they are not a standing convention.
Why not a dev branchβ
The dev (or develop) branch from Git Flow was designed to hold integrated-but-unreleased work for batched version cuts. Publica.la's SaaS services do not version, do not batch, and do not cut releases. In the 90 days preceding this decision (audit date 2026-04-17), across farfalla, coniglio, and medusa:
| Signal | farfalla | coniglio | medusa |
|---|---|---|---|
Unshipped commits on dev at audit time | 0 | 0 | 0 |
Commits on main (90d) | 1,848 | 153 | 216 |
Merges dev β main (90d) | 206 | 28 | 29 |
Merges into dev (90d) | 192 | 39 | 25 |
| Ratio (promoted : integrated) | 1.07 | 0.72 | 1.16 |
Median lag dev β main | ~10h | hours | hours |
| Direct-to-main / hotfix merges (90d) | 0 | 4 chores | 2 chores |
| Tags, CHANGELOG, Releases | none | none | none |
| Release-day ritual (cron, README, etc.) | none | none | none |
| Feature-flag infrastructure | stale | none | none |
dev held zero unshipped work across all three repos. The ratio of promotions-to-integrations is ~1:1: every commit merged into dev gets forwarded to main within hours. There is no release cycle for the ratio to serve. The branch was acting as a tollbooth (a merge commit, a CI pass, a staging bounce), not a reservoir (nothing accumulated).
The hidden value people sometimes attribute to a staging branch (catching bugs after merge to dev but before promotion to main) was explicitly checked: zero incidents of this kind in the audited window.
Separation of integration policy from deployment policyβ
Branches should not carry environment meaning when environments already have their own gates. main is an integration branch. staging and production are GitHub Environments with their own protection rules and reviewers. Using a second long-lived branch to approximate a second environment duplicates that machinery and produces the deadlocks documented in Deploy Approval Pattern.
What replaces the dev bounceβ
What dev was doing | Replacement |
|---|---|
| Triggering an auto-staging deploy on merge | Auto-staging deploy on merge to main |
| Preview-deploying a feature for integration testing | staging-review approval gate on the PR |
| Keeping incomplete work out of production | production environment reviewer gate + feature flags as needed |
| A spot to "park" risky work | Feature flags (Laravel Pennant recommended when needed) |
| A ritual signal that something is "ready for release" | PR review + staging smoke tests + production reviewer approval |
Exceptionsβ
Short-lived release/* or integration/* branches are allowed in specific cases:
- Coordinated multi-feature ship. Two or more PRs must land on production together after joint validation. Open a short-lived branch, merge the PRs into it, deploy to staging from it, then fast-forward to
main. Delete the branch. - Freeze window around a major event. Temporary stabilization period. Same mechanics as above; the branch dissolves when the window ends.
These are tactical, not architectural. They never outlive the work that justified them.
Things that are not exceptions:
- "I want a place to merge things while they stabilize." Use a feature flag.
- "We've always had
dev." Not a reason. The audit above is. - "Different teams need different mainlines." Split the repo.
Feature flagsβ
Trunk-based development is usually paired with a flag system for hiding in-flight user-visible work. Publica.la does not currently have load-bearing flag infrastructure, and the audit shows we have not been using branches for this either. We are not making flags a prerequisite for trunk-based.
When a change genuinely needs to ship half-baked (dark launch, progressive rollout, per-tenant toggle), adopt Laravel Pennant for that specific need. Don't retrofit flags across the codebase speculatively; that produces dead flag debt.
Rollbackβ
With trunk-based, production rollback is git revert + merge + redeploy. The production reviewer gate provides the safety valve before the bad deploy, and criceto provides the smoke test after. This is unchanged from the dev-based world; dev was not functioning as a rollback buffer in the audited data.
Branches across repos (at time of writing)β
| Repo | Trunk | Status |
|---|---|---|
farfalla-integrations | main only | Already trunk-based |
farfalla-https-guard | main only | Already trunk-based |
coniglio | main + dev | dev removal pending GitHub-migration completion (see below) |
medusa | main + dev | dev removal pending GitHub-migration completion (see below) |
farfalla | master + dev (GitLab) | Migrate to GitHub first; drop dev afterward, alongside the rest |
The order is finish the GitLab β GitHub migration first, drop dev second. Running the cutover runbook on coniglio or medusa ahead of the broader migration creates a second window of churn for the team to track and buys nothing operationally. Hold the cutover until every active repo is on GitHub, then sweep the dev-bearing repos one at a time.
Relatedβ
- Deploy Approval Pattern covers the split-gate staging-review flow under trunk.
- Dropping the
devBranch is the per-repo cutover runbook. - Migration Guide is the full GitLabβGitHub checklist.