Choosing a branching model for an open-source project Jump to heading
Open-source projects have constraints internal teams do not. Most contributors work from forks and cannot push branches to the main repository. Maintainers are few, volunteer their time, and review in bursts. Releases are versioned and users stay on old versions for years. Contributors arrive with no context, read the contributing guide once if at all, and give up if the process is confusing. A branching model that works well for a product team — develop branches, environment branches, elaborate release trains — becomes friction here. The model that serves most projects is simple: one default branch that is always in a releasable state, pull requests from forks, tags for releases, and release branches only when there are versions to support. This page walks through that choice and its supporting rules, within GitFlow vs GitHub Flow comparison.
When to use this approach Jump to heading
- You maintain an open-source project, or are starting one.
- Contributors find your branching rules confusing, or open pull requests against the wrong branch.
- You inherited a GitFlow setup and wonder whether
developearns its keep. - Your project ships versioned releases that users install.
Step 1 — Use one default branch that contributors target Jump to heading
Contributors open pull requests against the default branch, whatever it is called. Make that branch the one where development happens, so the default is right. A separate develop branch as the default and main for releases confuses newcomers — some target main anyway — and adds a merge step for maintainers.
gh repo edit --default-branch main
gh api "repos/$OWNER/$REPO" --jq .default_branch Step 2 — Keep the default branch releasable Jump to heading
Because users may install from the default branch and releases are cut from it, it must always pass the full test suite. Require checks on pull requests, test the merge result, and keep releases a matter of tagging.
# Required checks on main — runs for fork pull requests without secrets
on:
pull_request:
push:
branches: [main]
permissions: { contents: read }
jobs:
test:
strategy: { matrix: { python: ["3.10", "3.11", "3.12", "3.13"] } }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "${{ matrix.python }}" }
- run: pip install -e .[test] && pytest -q Fork pull requests run without secrets; make sure required checks do not need any, as covered in running CI for fork pull requests safely.
Step 3 — Release by tagging, with release branches only for supported versions Jump to heading
A release is a signed tag on the default branch. When an older version needs fixes after a newer one ships, create its release branch from the old tag at that point — not in advance.
git tag -s v3.2.0 -m "Release 3.2.0" && git push origin v3.2.0
# Only when 3.1 needs a fix after 3.2 has shipped:
git switch -c release/3.1 v3.1.4 && git push -u origin release/3.1 Creating release branches lazily means most projects have none, or one or two, rather than a branch per version nobody will ever patch. The maintenance side is covered in supporting multiple maintained versions.
Step 4 — Write the rules contributors actually need Jump to heading
Contributors need three things from the branching model: which branch to target, how to keep their pull request up to date, and whether to squash. Say it in a few lines in CONTRIBUTING.md, and nothing more.
## Pull requests
- Fork the repository and branch from `main`. Open your pull request against `main`.
- You do not need to rebase or squash; maintainers squash-merge.
- If your branch falls behind, merging `main` into it is fine. Squash merging on the maintainers’ side removes the need for contributors to learn interactive rebase, and keeps main readable regardless of how a contributor’s branch looks. The trade-offs are in squash vs merge vs rebase decision matrix.
Step 5 — Reduce maintainer load with automation Jump to heading
Maintainers’ time is the scarcest resource in most projects. Automate the parts of the model that do not need judgement: labelling pull requests by area, closing stale ones politely, generating release notes from merged pull requests, and backporting labelled fixes to supported release branches.
# Release notes from merged PR titles and labels, generated by the forge on tag
# .github/release.yml
changelog:
categories:
- { title: "Breaking changes", labels: ["breaking"] }
- { title: "Features", labels: ["enhancement"] }
- { title: "Fixes", labels: ["bug"] }
- { title: "Other", labels: ["*"] } Related automation is covered in auto-labelling pull requests by changed path and closing stale branches and pull requests.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is GitFlow ever right for open source? Jump to heading
For large projects with scheduled releases and dedicated release managers, a stabilisation branch can help. Even then, a release branch cut from main before each release achieves the same without making contributors target a separate develop branch.
Should maintainers push directly to main? Jump to heading
Going through pull requests keeps CI and review consistent for everyone, maintainers included. Small projects sometimes allow maintainers to push trivial changes; require CI to pass regardless.
What about signed commits from contributors? Jump to heading
Requiring contributors to sign commits raises the bar to contributing considerably. Most projects instead sign release tags and rely on review; the options are discussed in handling unsigned commits from outside contributors.
Related Jump to heading
- GitFlow vs GitHub Flow Comparison — the parent topic.
- Running Releases with GitHub Flow — the closest internal-team equivalent.
- Signed Tags vs Signed Commits — why release tags matter most for open source.
- Generating Pull Request Descriptions from Commits — helping contributors explain changes.