Naming conventions for feature branches Jump to heading

Branch names look like a matter of taste until automation starts reading them. CI decides what to deploy by branch prefix. Issue trackers link branches by key. Cleanup jobs skip anything starting with release/. Merge request templates fill in the issue from the branch. When names are inconsistent — johns-fix, Feature/PAY_812, new-export-stuff-v2 — every one of those tools needs exceptions, and some simply fail. A convention that encodes the type of work, the issue it belongs to and a short description, in a fixed shape, makes branches readable for people and parseable for scripts. This page sets one up, enforces it at the right points and puts it to use, within feature branch isolation.

When to use this approach Jump to heading

  • Branch names in your repositories have no consistent shape.
  • CI, deploy or cleanup automation keys off branch names and keeps needing special cases.
  • You want pull requests and commits linked to issues without manual effort, as in requiring linked issues on pull requests.
  • Long-lived branches — releases, environments — need names that are never confused with feature work.

Step 1 — Pick a shape: type, key, slug Jump to heading

A practical convention has three parts: a type prefix that says what kind of work it is, the issue key, and a short lower-case slug. Long-lived branches use separate, reserved prefixes.

feat/PAY-812-export-scheduling
fix/PAY-907-credit-note-rounding
chore/OPS-221-upgrade-node-20
docs/DOC-44-signing-guide

release/2.4            # reserved: release branches
hotfix/2.4.1           # reserved: urgent fixes on a release
env/staging            # reserved: deployment branches
The parts of a branch nameThe type prefix groups branches by kind of work and lets automation apply rules per type. The issue key links the branch to the tracker. The slug tells people what the work is. Reserved prefixes for releases and environments keep long-lived branches distinct from feature work.type/feat, fix, choreKEY-123tracker link-short-slugfor peoplereservedrelease/, env/hotfix/lower-case, hyphens, no spaces — every tool handles that

Keep types few — four to six — and aligned with your commit message types if you use Conventional Commits, so the branch and its commits tell the same story.

Step 2 — Write the rule as one regular expression Jump to heading

A single pattern, kept in one place, is what every enforcement point reads. Make it strict enough to be useful and simple enough to explain in one sentence.

# .git-branch-pattern — the single source of the naming rule
^((feat|fix|chore|docs|refactor|test)/[A-Z][A-Z0-9]+-[0-9]+-[a-z0-9]+(-[a-z0-9]+)*|release/[0-9]+\.[0-9]+|hotfix/[0-9]+\.[0-9]+\.[0-9]+|env/[a-z]+|main)$
# Verification: test the pattern against real and invented names
pattern=$(cat .git-branch-pattern)
for b in feat/PAY-812-export-scheduling fix/pay-1-x johns-fix release/2.4; do
  printf '%s\t' "$b"; printf '%s\n' "$b" | grep -Eq "$pattern" && echo ok || echo REJECT
done

Step 3 — Enforce early on the client, finally on the server Jump to heading

Check the name when someone pushes a new branch, with a clear message showing the expected shape. A pre-push hook catches it before the push; the server rule catches pushes that skip hooks.

# pre-push: reject new remote branches whose names break the convention
pattern=$(cat "$(git rev-parse --show-toplevel)/.git-branch-pattern")
zero=$(git hash-object --stdin </dev/null | tr '0-9a-f' '0')
while read -r lref loid rref roid; do
  case "$rref" in refs/heads/*) ;; *) continue ;; esac
  [ "$roid" = "$zero" ] || continue                       # only check new branches
  name=${rref#refs/heads/}
  printf '%s\n' "$name" | grep -Eq "$pattern" || {
    echo "Branch name '$name' does not match the convention, e.g. feat/PAY-812-export-scheduling"; exit 1; }
done
Where branch names are enforcedThe developer's pre-push hook catches a bad name before anything is sent, with a helpful example. The server's branch name rule or push rule rejects whatever skipped the hook. Automation that reads branch names can then rely on the shape without defensive parsing.from friendly to authoritativepre-push hookearly, with an exampleServer rule / push rulecannot be skippedAutomationparses names with confidenceonly new branches are checked — renaming old ones is a separate, optional cleanup

On the server, use a ruleset restricting branch names, GitLab’s branch name push rule, or an update hook, as covered in writing an update hook for per-branch policy.

Step 4 — Make the right name the easy name Jump to heading

Most bad names come from typing. A small helper that builds the name from a type, a key and a title removes the friction.

# git-new <type> <KEY-123> <title words…>
git config --global alias.new '!f() {
  t=$1 k=$2; shift 2
  slug=$(printf "%s" "$*" | tr "[:upper:]" "[:lower:]" | tr -cs "a-z0-9" "-" | sed "s/^-//; s/-$//" | cut -c1-40)
  git switch -c "$t/$k-$slug"; }; f'
git new feat PAY-812 Export scheduling per account
# Switched to a new branch 'feat/PAY-812-export-scheduling-per-account'

Many trackers and forges can also create a branch from an issue with the key already in the name; encourage that path.

Step 5 — Use the structure in automation Jump to heading

With predictable names, automation becomes simple pattern matching.

# Deploy previews only for feature and fix branches
on:
  push:
    branches: ["feat/**", "fix/**"]

# Label pull requests by branch type
      - run: gh pr edit "$PR" --add-label "type:${BRANCH%%/*}"
        env: { BRANCH: "${{ github.head_ref }}", PR: "${{ github.event.number }}", GH_TOKEN: "${{ github.token }}" }
# Prefill the issue key in commit messages from the branch name
key=$(git symbolic-ref --short HEAD | sed -n 's#^[a-z]*/\([A-Z][A-Z0-9]*-[0-9]*\)-.*#\1#p')

The commit-message side of this is covered in generating a commit message template per branch.

Branch names matching the convention over timeAn illustrative repository after introducing a naming rule. Existing branches keep their old names, so the share rises gradually as new branches follow the rule and old ones merge or are cleaned up, reaching nearly all active branches within a quarter.active branches matching the convention (illustrative)at introduction31%month 168%month 287%month 397%no renaming campaign needed — normal branch turnover does the work

Step 6 — Document the convention where people create branches Jump to heading

The rule only helps if people know it at the moment they create a branch. Put a one-line summary and an example in the contributing guide, in the hook’s error message and in the pull request template, so the convention is visible at every step where a name is chosen.

<!-- CONTRIBUTING.md -->
Branch names: `type/KEY-123-short-slug`, e.g. `feat/PAY-812-export-scheduling`.
Types: feat, fix, chore, docs, refactor, test. Create one with `git new feat PAY-812 Export scheduling`.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Should usernames be part of branch names? Jump to heading

Rarely. They duplicate information the forge already records, and branches often change hands. Use them only for personal scratch namespaces, such as u/alice/…, and exclude those from CI and deploy rules.

What about existing branches that break the convention? Jump to heading

Leave them; only new branches are checked. They will be merged or deleted in time. Renaming active branches disrupts open pull requests for little gain.

Are slashes in branch names a problem? Jump to heading

They work everywhere modern, and they let tools group branches as if in folders. The one constraint is that feat and feat/x cannot both exist, because Git stores refs as paths — another reason to keep type prefixes fixed.