Branch Naming

The type/kebab-case branch naming convention enforced at pre-push.

Branches in this monorepo follow a strict <type>/<kebab-case-description> shape. The check runs on git push: if the branch you are pushing does not match the pattern, the push is rejected before it ever touches the remote.

Source of truth. Branch validation lives in .husky/pre-push:5-33. If this page and that script disagree, the script wins.

The pattern

text
<type>/<kebab-case-description>
1 line of text code
  • type — one of the allowed prefixes below. Same list that commitlint uses for commit types, minus the ones that never make sense for a branch (style, perf, build, revert).
  • description — lower-case letters and digits, separated by hyphens. No spaces, no underscores, no slashes, no camelCase. Must start with a letter or digit.

The exact regex enforced by the hook is:

text
^(feat|fix|chore|ci|docs|refactor|test|variant)\/[a-z0-9][a-z0-9-]*$
1 line of text code

Allowed type prefixes

PrefixUse when...
featYou are building a new feature on this branch.
fixYou are tracking down and fixing a bug.
choreRoutine maintenance: dependency bumps, tooling tidy-ups, housekeeping that touches no public behaviour.
ciYou are changing CI configuration — workflows, husky hooks, turbo pipelines, release automation.
docsYou are working on documentation only — MDX pages, READMEs, concept notes.
refactorYou are restructuring code without changing its observable behaviour.
testYou are adding or updating tests.
variantYou are adding or adjusting a CVA variant on a UI component in @itu/ui.

Why the convention exists

Branch naming is not bureaucracy for its own sake — the pattern unlocks several things:

  • Predictable automation hooks. CI jobs can key off the prefix: docs/* branches skip the full test matrix, ci/* branches trigger a self-check, feat/* branches run the full suite. Without a convention, every workflow would need a manually maintained list.
  • CI routing and triage. When a branch fails on CI, the prefix tells the on-call reviewer what kind of change they are looking at before they open the diff. A red chore/update-deps is triaged differently than a red fix/search-url-html-strip.
  • Human readability in the branch list. git branch -a is easier to scan when every branch starts with a verb-sized prefix. You can find "all the doc branches" at a glance.
  • Consistency with commit types. Using the same vocabulary for branches and commits means a single mental model — not two.
  • Kebab-case is URL-safe. Branch names end up in GitHub URLs, CI job names, and Slack notifications. Hyphens render cleanly everywhere; underscores and camelCase do not.

Passing examples

text
feat/documentation-website
feat/add-playground-controls
fix/search-url-html-strip
fix/button-hover-state
docs/contributor-workflow
chore/update-deps
variant/links-grid-compact
refactor/extract-cva-factory
test/datatable-sort-order
ci/add-a11y-workflow
10 lines of text code

All of these match the pattern: allowed prefix, slash, kebab-case description starting with a letter or digit.

Failing examples

Each failure below is the actual error message the hook prints before exit 1. The message comes straight from .husky/pre-push.

Wrong prefix

text
feature/add-playground-controls
1 line of text code
text
ERROR: Branch name 'feature/add-playground-controls' does not match the naming convention.
 
Pattern: <type>/<kebab-case-description>
Types: feat, fix, chore, ci, docs, refactor, test, variant
 
Examples:
feat/documentation-website
fix/button-hover-state
variant/links-grid-compact
chore/update-deps
10 lines of text code

feature is not in the allowed list — use feat instead.

No slash

text
feat-add-playground-controls
1 line of text code
text
ERROR: Branch name 'feat-add-playground-controls' does not match the naming convention.
 
Pattern: <type>/<kebab-case-description>
Types: feat, fix, chore, ci, docs, refactor, test, variant
 
Examples:
feat/documentation-website
fix/button-hover-state
variant/links-grid-compact
chore/update-deps
10 lines of text code

The type and the description have to be separated by a slash, not a hyphen.

Upper-case or camelCase description

text
feat/AddPlaygroundControls
1 line of text code
text
ERROR: Branch name 'feat/AddPlaygroundControls' does not match the naming convention.
 
Pattern: <type>/<kebab-case-description>
Types: feat, fix, chore, ci, docs, refactor, test, variant
 
Examples:
feat/documentation-website
fix/button-hover-state
variant/links-grid-compact
chore/update-deps
10 lines of text code

Descriptions must be kebab-case — lower-case, hyphen-separated.

Underscores instead of hyphens

text
feat/add_playground_controls
1 line of text code
text
ERROR: Branch name 'feat/add_playground_controls' does not match the naming convention.
 
Pattern: <type>/<kebab-case-description>
Types: feat, fix, chore, ci, docs, refactor, test, variant
 
Examples:
feat/documentation-website
fix/button-hover-state
variant/links-grid-compact
chore/update-deps
10 lines of text code

Underscores are not allowed — use hyphens.

Exempt branches

A handful of branch names bypass the check entirely. The hook uses a case statement against the current branch and ;; skips validation when the branch matches any of these patterns:

PatternWhy it is exempt
mainThe default integration branch. Every release is cut from here — it can't be renamed to match a <type>/<desc> pattern without breaking every clone, CI workflow, and deploy pipeline in the monorepo.
developLong-lived integration branch used alongside main for staged rollouts. Same rationale as main — it is a permanent fixture, not a topic branch.
release/*Release branches cut from main at the start of a release candidate cycle (e.g. release/2026-Q2). These are not topic branches and should not pretend to be — they are version-scoped.
hotfix/*Emergency fixes that branch off a release tag and merge back into both main and the release branch. hotfix is its own lifecycle; it reads clearly without being shoehorned into a type prefix.

The exempt list is deliberately small. Everything else — including personal scratch branches, WIP spikes, and experiments — has to follow the convention.

When the hook runs

The check is part of .husky/pre-push, which fires every time you run git push. If you have never pushed a branch before, the hook runs once the branch is named and tracked — you do not need a remote to trigger it locally during git push.

If you realise mid-work that your branch is misnamed, rename it before pushing:

bash
git branch -m old-name feat/new-name
1 line of bash code

Related

  • Commit messages — the sibling convention that commitlint enforces on every commit.
  • Git hooks — the full tour of husky hooks, including what pre-push does after the branch check passes.