DEV Community

Cover image for A Practical GitFlow Setup That Works on GitHub

A Practical GitFlow Setup That Works on GitHub

I use GitFlow even for my side projects.

Not because it is elegant. Not because it is modern. I use it because it forces me to behave like the project matters, even when nobody else is watching.

When you work alone, shortcuts are tempting. You push directly to main. You skip reviews. You promise yourself you will clean it up later. Later never comes. GitFlow, with the right rules, removes that option. It makes discipline the default instead of a personal choice.

This is important to say upfront. GitFlow is not light. It adds structure, friction, and process. If you deploy to production ten times per day, GitFlow will slow you down. In that case, trunk-based development with feature flags is usually the better choice.

But when you ship versioned releases, when you have a real staging phase, or when you want a clean separation between “work in progress” and “ready to release”, GitFlow still works well. Even for a solo developer.

Everything in this article is what I actually run on GitHub today. On client projects (when they want to use GitFlow, of course). On open source. On side projects that may never make money.

If you are going to use GitFlow, use it properly. Otherwise, do not use it at all.

GitFlow Init

2. When GitFlow Makes Sense and When It Does Not

GitFlow is a release model, not a productivity hack.

I use it when the way I ship software matches the assumptions GitFlow makes. If those assumptions are wrong, the model fights you every day.

When GitFlow makes sense

GitFlow works well for me in these cases.

You ship versioned releases. You care about v1.2.3 meaning something. You want a clear moment where a release is cut, stabilized, tested, and then shipped.

You have a real staging phase. Not “production with a flag”, but an environment where release candidates live, get tested, and sometimes get rejected.

You need parallel workstreams. New features continue on develop while a release is being stabilized, or while a hotfix is being prepared.

You want a hard separation between work in progress and releasable code. main always deployable. develop allowed to move faster and break occasionally.

In these scenarios, GitFlow reduces stress instead of adding it. The branches reflect reality.

When GitFlow is the wrong choice

I avoid GitFlow in these situations.

You deploy to production continuously. Multiple times per day. No staging. No release windows. In that world, GitFlow adds branches without adding safety.

Your team already struggles to keep develop stable. GitFlow assumes you can treat develop as an integration gate. If you cannot, you are better off with trunk-based development and feature flags.

You do not have environments to support it. No staging. No separation between integration and production. In that case, GitFlow becomes naming theater.

The most common mistake

The worst setup I see is this.

Teams adopt GitFlow “by the book”, but keep their old habits. Direct pushes sneak in. Hotfixes do not get merged back. Releases happen straight from develop because it is “almost ready”.

GitFlow Workflow

3. The GitFlow Branch Model I Actually Use on GitHub

I keep the branch model boring and strict. No variations. No “almost GitFlow”. If I start bending the rules, the model stops working.

These are the only branches I use.

main

main is always releasable. Always.

I never push directly to it. Not even for a typo. If main is broken, the whole model failed upstream.

What this gives me is simple. At any point in time, I can tag main and ship. No excuses. No cleanup phase.

If you cannot guarantee this, GitFlow gives you nothing.

develop

develop is my integration branch.

This is where features meet. This is where conflicts show up early. This is where I accept temporary instability, but only temporarily.

I treat develop as protected, even when I work alone. No direct pushes. Always via pull request. That PR is my forced pause to think: is this actually ready to be integrated?

If develop stays broken for days, GitFlow collapses. Integration must be frequent and boring.

feature/*

All work starts here.

Feature branches are short-lived by design. If a feature takes more than three to five days, I split it. Long-lived feature branches are the biggest source of merge pain and hidden risk.

I do not optimize for “perfect feature branches”. I optimize for deleting them quickly.

Examples:

  • feature/auth-token-refresh
  • feature/api-rate-limits
  • feature/ui-settings-page

Once merged into develop, the branch is gone. No reuse. No resurrection.

release/*

I only create a release/* branch when I actually need stabilization.

If there is no staging phase, I skip release branches entirely. Creating them without a purpose is pure ceremony.

A release branch exists to say one thing: no new features. Only fixes, hardening, documentation, and versioning.

Typical names:

  • release/1.4.0
  • release/2026.02

When the release is ready, it gets merged into main, tagged, and then merged back into develop. Always both. This is where many teams drift.

hotfix/*

Hotfixes are rare. That is intentional.

A hotfix starts from main, never from develop. It fixes production. Nothing else.

After the fix, I merge it into main and immediately back into develop. Skipping the back-merge is the fastest way to reintroduce the same bug later.

Names stay explicit:

  • hotfix/login-nullref
  • hotfix/payment-timeout

Hotfixes are treated with more care, not less.

4. The Rules That Make GitFlow Work (And Why I Enforce Them on Myself)

GitFlow without rules is just branch naming.

I enforce the same rules on my side projects that I enforce on client projects. The reason is simple. If the setup only works when I am “careful”, it will fail under pressure.

Protected branches are non-negotiable

I protect main and develop. Always.

  • No direct pushes.
  • No force pushes.
  • No branch deletion.
  • Pull requests only.

Even when I am the only contributor.

This removes temptation. I cannot “just push a quick fix”. Every change goes through the same path. That consistency is the real value.

Pull requests are mandatory checkpoints

A pull request is not about collaboration. It is about forcing a decision point.

Before merging, I answer a few basic questions:

  • Why does this change exist?
  • What exactly changed?
  • How did I verify it works?
  • What is the risk?

If I cannot answer those clearly in a PR description, the change is not ready.

This sounds heavy. In practice, it saves time. It avoids “I’ll fix it later” commits.

GitHub Protection Branches

Approvals still matter, even solo

I require at least one approval.

Yes, I approve my own PRs. That is not the point.

The point is the pause. The forced context switch. Opening the PR, reading the diff in GitHub, and approving it is different from pushing code from an editor.

That pause catches mistakes more often than people expect.

Conversation resolution is underrated

I require all conversations to be resolved before merging.

This prevents half-finished reviews and “we will come back to this” comments that never get addressed.

If a comment exists, it deserves a decision. Fix it. Reject it. Explain it. Then resolve it.

Linear history vs merge commits

This is where teams hurt themselves.

If your team is comfortable with rebasing and understands it well, require a linear history. It keeps develop clean.

If not, allow merge commits. But standardize it. Do not mix strategies per PR.

I personally use:

  • Squash merge for feature/* into develop
  • Merge commits for release/* and hotfix/* into main

The key is not which option you pick. The key is that it is enforced, not debated every time.

5. Pull Requests as the Core Unit of Work

In my setup, branches exist to create pull requests. Pull requests are the real workflow.

If you get PRs right, GitFlow becomes manageable. If PRs are large, vague, or inconsistent, GitFlow falls apart.

A boring PR template on purpose

I use the same PR template everywhere. It is intentionally simple.

  • Why: the problem or reason for the change
  • What: what changed at a high level
  • How tested: tests run, manual checks, environments
  • Risk: what could break
  • Rollback: how to undo it if needed

This is not documentation. It is a forcing function. If I cannot fill this in quickly, the PR is probably doing too much.

Small PRs beat clever branch strategies

If a reviewer needs more than 30–45 minutes, the PR is too big.

Large PRs hide bugs. They also create social pressure to approve without fully understanding the change. That is how regressions slip in.

I optimize for PRs that can be reviewed quickly and confidently. That usually means slicing features into vertical, mergeable steps.

GitFlow does not fix big PRs. It amplifies the pain if you allow them.

CODEOWNERS even for tiny repos

I use CODEOWNERS even when I am the only owner.

It makes review routing explicit. It also prepares the repo for future contributors without rethinking the rules.

More importantly, GitHub treats CODEOWNERS reviews differently. That gives you another enforcement layer, not another convention.

6. Naming, Traceability, and Future Me

Most conventions exist for one reason. You will forget.

Future me is the main consumer of my Git history. Not GitHub. Not tools. Me, six months later, trying to understand why something exists.

Branch names should explain intent

I use explicit branch names. Not clever ones.

feature/login tells me nothing.
feature/oauth-token-refresh tells me exactly what problem was being solved.

If I use an issue tracker, I include the ID:

  • feature/ABC-123-oauth-token-refresh

This costs seconds. It saves minutes every time I search history.

PR titles are part of the changelog

I treat PR titles as public API.

Release notes, changelogs, and audit trails are generated from them. If the title is vague, everything downstream becomes vague.

I avoid “fix stuff” or “cleanup”. I prefer:

  • “Fix token refresh race condition”
  • “Add rate limiting to public API”

If I cannot summarize the change in one sentence, the PR is probably too large.

Link everything, even when it feels redundant

I always link PRs to issues. Always.

Even on side projects. Even when the issue only exists to track the PR.

GitHub already gives you the syntax. Use it.
Closes #123 is enough.

This creates a navigable graph. Branch to PR. PR to issue. Issue to release. Without links, GitFlow loses one of its biggest advantages.

Traceability must be enforced, not suggested

If traceability is optional, it will be skipped.

I enforce naming and linking through:

  • Branch naming patterns
  • PR templates
  • Required checks

The goal is not bureaucracy. The goal is predictability.

Why this matters more with GitFlow

GitFlow creates more branches than trunk-based development. That increases cognitive load.

Naming and traceability reduce that load. They make the model survivable over time.

In the next section, I will map this structure to CI/CD pipelines, and explain what I run where, and why.

GitFlow on GitKraken

6. Naming, Traceability, and Future Me

Most conventions exist for one reason. You will forget.

Future me is the main consumer of my Git history. Not GitHub. Not tools. Me, six months later, trying to understand why something exists.

Branch names should explain intent

I use explicit branch names. Not clever ones.

feature/login tells me nothing.
feature/oauth-token-refresh tells me exactly what problem was being solved.

If I use an issue tracker, I include the ID:

  • feature/ABC-123-oauth-token-refresh

This costs seconds. It saves minutes every time I search history.

PR titles are part of the changelog

I treat PR titles as public API.

Release notes, changelogs, and audit trails are generated from them. If the title is vague, everything downstream becomes vague.

I avoid “fix stuff” or “cleanup”. I prefer:

  • “Fix token refresh race condition”
  • “Add rate limiting to public API”

If I cannot summarize the change in one sentence, the PR is probably too large.

Link everything, even when it feels redundant

I always link PRs to issues. Always.

Even on side projects. Even when the issue only exists to track the PR.

GitHub already gives you the syntax. Use it.
Closes #123 is enough.

This creates a navigable graph. Branch to PR. PR to issue. Issue to release. Without links, GitFlow loses one of its biggest advantages.

Traceability must be enforced, not suggested

If traceability is optional, it will be skipped.

I enforce naming and linking through:

  • Branch naming patterns
  • PR templates
  • Required checks

The goal is not bureaucracy. The goal is predictability.

8. Releases, Tags, and Changelogs I Can Trust

Releases are not a side effect. They are a deliberate action.

GitFlow only pays off if releases are predictable and traceable. That starts with being strict about where and how you tag.

Tags exist only on main

I tag releases only on main. No exceptions.

If a tag exists anywhere else, it lies. A tag should mean “this is what went to production” or “this is the shipped artifact”.

Tagging on develop or release/* breaks that guarantee and creates confusion later when you try to answer a simple question: what is actually running?

Release branches are not releases

A release/* branch is a candidate. Not a promise.

It can be delayed. It can be fixed. It can even be abandoned. That is fine.

The release only exists when:

  1. The release branch is merged into main
  2. A tag is created on main

Anything else is pre-release noise.

Semantic Versioning

Versioning decisions are made early

I decide versioning strategy at the start of a project, not during the first release.

  • Semantic versioning when public contracts matter
  • Date-based versioning for internal tools or fast-moving products

Changing versioning mid-stream creates churn and inconsistent expectations. Pick one and live with it.

Changelogs are generated, not remembered

I do not write changelogs by hand.

They are generated from:

  • PR titles
  • Labels
  • Commit history on main

This is why PR titles matter. Sloppy titles produce useless release notes.
(Actually GitHub Copilot helps me a lot to create these ones and commit messages)

9. Environments or GitFlow Is Just Ritual

GitFlow assumes environments. Without them, you are just renaming branches.

I see many setups where GitFlow exists on paper, but everything deploys to the same place. In that case, the extra branches add zero value.

The minimum environment setup I use

I keep it simple. Three environments.

  • Dev / Integration
    Automatically deployed from develop. This is where features meet and conflicts surface early.

  • Staging
    Deployed from release/*. This environment exists to say “no”. If something fails here, the release is not ready.

  • Production
    Deployed from main. Always. No exceptions.

If you cannot support at least this, GitFlow will not protect you.

Why each environment matters

develop needs a shared place where the system actually runs. Unit tests are not enough. Integration bugs appear only when components talk to each other.

release/* needs a stable environment. The whole point of a release branch is to freeze scope and validate reality.

main maps one-to-one with production. If you blur this line, you lose confidence in every tag and every rollback.

GitFlow without environments is overhead

Branches alone do not reduce risk. Feedback loops do.

If all feedback arrives only after merging to main, GitFlow becomes slower trunk-based development, nothing more.

In the next section, I will explain how I handle hotfixes, and why this is where most teams break the model.

10. Handling Hotfixes Without Breaking the Model

Hotfixes are where GitFlow discipline is tested.

They arrive under pressure. Something is broken. Time matters. This is exactly when shortcuts feel justified. That is also when long-term damage happens.

Hotfixes start from main

A hotfix always branches from main. Never from develop.

main represents what is running in production. If you start a hotfix anywhere else, you are guessing what you are fixing.

The goal of a hotfix is simple. Fix production. Nothing more.

Scope is deliberately small

A hotfix branch contains the minimal change required to restore service.

No refactoring. No cleanup. No “since we are here” improvements. Those belong on develop or in a future release.

Every extra change increases risk at the worst possible moment.

CI and reviews still apply

I do not disable checks for hotfixes.

Pipelines might be slightly faster. Reviews might be faster. But rules still apply.

Hotfixes are urgent, not special.

Why teams drift here

Under pressure, people optimize for now. GitFlow optimizes for later.

Skipping the back-merge or bypassing rules feels harmless in the moment. It creates invisible debt that surfaces weeks later.

If your GitFlow setup cannot survive hotfix pressure, it is not real.

Merge Conflicts

11. Merge Conflicts and How I Keep Them Boring

Merge conflicts are not a Git problem. They are a workflow problem.

Every painful conflict I have dealt with had the same root cause. Branches lived too long.

Short-lived branches solve most conflicts

I keep feature branches short. Days, not weeks.

If a feature cannot be merged within three to five days, I split it. Vertical slices. Mergeable steps. Incremental progress.

This single habit reduces conflicts more than any Git command ever will.

12. GitHub Actions Patterns I Reuse Everywhere

Automation is what makes GitFlow survivable long term.

If the workflow depends on people remembering rules, it will degrade. GitHub Actions are there to make the right path the default path.

Required checks are gates, not guidelines

Every protected branch has required checks.

If a check is required, the PR cannot merge. No override. No “just this once”.

This removes social pressure. The system says no, not a person.

I treat required checks as part of the branch contract. If they are flaky or slow, I fix the checks. I do not remove the requirement.

Reusable workflows or nothing

I do not copy-paste workflows across repositories.

I use reusable workflows so:

  • The same rules apply everywhere
  • Changes are made once
  • Drift is impossible

Environment protection is part of GitFlow

For production deployments, I use GitHub environments with protection rules.

  • Required reviewers
  • Scoped secrets
  • Explicit approval step

This adds a final safety net without bypassing GitFlow.

If main deploys to production, production must be protected.

Secrets and permissions are minimal

Workflows get only the permissions they need. No wildcard tokens. No long-lived secrets when OIDC is available.

This matters more as automation grows. GitFlow increases automation surface. Least privilege keeps that surface manageable.

13. Security and Governance I Enable by Default

Security and governance are not enterprise-only concerns. I enable them even on side projects. Especially on side projects.

If the repo matters enough to use GitFlow, it matters enough to protect it.

Dependabot is always on

I enable Dependabot alerts and PRs by default.

  • Alerts tell me when I am exposed.
  • PRs force the fix through the same PR rules as any other change.

I never update dependencies directly on protected branches. Updates go through PRs, checks, and reviews like everything else.

Code scanning is part of the pipeline

At minimum, I run code scanning on:

  • develop
  • main

This catches obvious issues early and prevents shipping known problems.

I do not chase perfect security scores. I want signal, not noise. But no scanning at all is not an option.

14. In a nutshell, what I do (copy this section)

This is the setup I apply by default. On client projects. On open source. On side projects.

Branches

  • main
    Always releasable. Production only.

  • develop
    Integration branch. Can move fast, but must stay usable.

  • feature/*
    Short-lived. One purpose. Merged into develop.

  • release/*
    Created only when a real stabilization phase exists.

  • hotfix/*
    Urgent fixes from main. Always merged back.

Protection rules

Protected branches:

  • main
  • develop
  • release/*
  • hotfix/*

Rules applied:

  • Pull requests required
  • At least one approval
  • Required status checks
  • Conversation resolution required
  • No direct pushes
  • No force pushes
  • No branch deletion

Pull request rules

  • Mandatory PR template: Why, What, How tested, Risk, Rollback
  • Small PRs by design
  • CODEOWNERS enabled
  • One merge strategy enforced

Merge strategy

  • Squash merge for feature/* into develop
  • Merge commits for release/* and hotfix/*
  • No rebasing of shared branches

CI/CD

  • feature/*: build, unit tests, lint
  • develop: full test suite, security checks
  • release/*: full suite, packaging, deploy to staging
  • main: production deploy, tag, changelog

Releases

  • Tags on main only
  • Semantic or date-based versioning decided early
  • Changelogs generated from PR metadata

Environments

  • develop → dev/integration
  • release/* → staging
  • main → production

Hotfix discipline

  • Branch from main
  • Minimal scope
  • Merge to main
  • Merge back to develop
  • No skipped steps

This policy is intentionally boring. Boring scales.


👀 GitHub Copilot quota visibility in VS Code

If you use GitHub Copilot and ever wondered:

  • what plan you’re on
  • whether you have limits
  • how much premium quota is left
  • when it resets

I built a small VS Code extension called Copilot Insights.

It shows Copilot plan and quota status directly inside VS Code.

No usage analytics. No productivity scoring. Just clarity.

👉 VS Code Marketplace:
https://marketplace.visualstudio.com/items?itemName=emanuelebartolesi.vscode-copilot-insights

Top comments (0)