Skip to content

Branching Strategy

This document defines the standard branching strategy used across software engineering projects.

Polk County’s Software Engineering Team uses GitHub Flow as the default branching model. GitHub Flow is a lightweight workflow centered around short-lived feature branches, pull requests, and frequent integration into the main branch.

This strategy supports rapid iteration, continuous integration, and a stable deployment-ready main branch.

---
config:
  theme: base
  gitGraph:
    showCommitLabel: false
---
gitGraph
   commit
   branch "feature 1"
   branch "feature 2"
   checkout "feature 1"
   commit
   commit
   checkout main
   merge "feature 1" tag: "v0.1.0"
   checkout "feature 2"
   commit
   commit
   checkout main
   merge "feature 2" tag: "v0.2.0"
   branch "hotfix"
   commit
   checkout main
   merge "hotfix" tag: "v0.2.1"
   branch "feature 3"
   commit
   commit
   commit
   checkout main
   merge "feature 3" tag: "v1.0.0"

The following principles apply to all repositories using the GitHub Flow branching strategy:

  • Keep main in a deployable state. It contains the latest stable version of the codebase.
  • No direct commits are made to main. All changes go through feature branches and pull requests.
  • Each new feature, bug fix, or improvement is developed in a separate branch created from main.
  • Open a pull request for all changes before merging.
  • Use peer review and automated validation as part of the merge process.
  • Automated checks should pass before a pull request is merged.
  • Prefer small, incremental changes over large, long-lived branches.
  • Squash merge pull requests to maintain a clean commit history.
  • Delete branches after merging to keep the repository clean.

The standard GitHub Flow process is:

  1. Create a branch from main

    Use a short-lived branch for the change.

  2. Make and commit changes

    Keep commits focused and descriptive, following the Conventional Commits standard.

  3. Push the branch to GitHub

    Publish the branch so a pull request can be opened.

  4. Open a pull request

    Describe the purpose of the change and any relevant context.

  5. Run automated checks

    CI workflows should validate the proposed changes.

  6. Complete review

    Address reviewer feedback, update the branch from the latest origin/main when needed, and confirm readiness to merge.

  7. Merge into main

    Merge only after approval and successful validation.

    Squash merging is preferred to maintain a clean history.

  8. Delete the branch

    Remove the short-lived branch after merge.

Updating a Feature Branch After Opening a PR

Section titled “Updating a Feature Branch After Opening a PR”

When a developer has already opened a pull request and later discovers that their feature branch is behind origin/main, the preferred approach is usually to rebase the feature branch onto the latest origin/main and then push the updated branch back to the PR.

This keeps the pull request focused on the feature work and avoids adding an extra merge commit from origin/main into the branch.

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    branch feature-branch
    checkout feature-branch
    commit id: "E"
    commit id: "F"

In this state, the PR is open from feature-branch into main.

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    branch feature-branch
    checkout feature-branch
    commit id: "E"
    commit id: "F"
    checkout main
    branch other-feature
    checkout other-feature
    commit id: "G"
    commit id: "H"
    checkout main
    merge other-feature id: "M1"

At this point, feature-branch is behind origin/main and needs to be updated before the PR can be merged.

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    commit id: "M1"
    checkout main
    branch feature-branch
    checkout feature-branch
    commit id: "E'"
    commit id: "F'"

After the rebase, the feature commits are replayed on top of the latest origin/main.

Commits E' and F' are rewritten versions of the earlier commits E and F. The branch name stays the same, but the commit IDs change after the rebase.

Terminal window
git checkout feature-branch
git fetch origin
git rebase origin/main

If Git reports conflicts during the rebase:

Terminal window
# resolve conflicts in your editor
git add <resolved-files>
git rebase --continue

Repeat that process until the rebase finishes.

Then update the branch on the remote:

Terminal window
git push --force-with-lease origin feature-branch

A rebase rewrites commit history, so a normal push will usually be rejected. --force-with-lease is the safer way to update the remote branch because it avoids overwriting unexpected remote changes.

Why rebase is usually the best choice for an open PR

Section titled “Why rebase is usually the best choice for an open PR”

Rebasing usually gives the cleanest pull request because it:

  • keeps history linear
  • avoids a merge commit from origin/main
  • makes the PR diff easier to review
  • presents the feature as if it were built on the latest origin/main

Alternative approach: merge origin/main into the feature branch

Section titled “Alternative approach: merge origin/main into the feature branch”

Some teams prefer merging instead of rebasing.

Terminal window
git checkout feature-branch
git fetch origin
git merge origin/main
git push origin feature-branch
gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    branch feature-branch
    checkout feature-branch
    commit id: "E"
    commit id: "F"
    checkout main
    branch other-feature
    checkout other-feature
    commit id: "G"
    commit id: "H"
    checkout main
    merge other-feature id: "M1"
    checkout feature-branch
    merge main id: "M2"

This is valid, but it adds a merge commit to the PR, which can make review history noisier.

For a developer-owned feature branch with an open pull request, the best default is:

  1. Fetch the latest refs from the remote.
  2. Rebase the feature branch onto origin/main.
  3. Resolve any conflicts.
  4. Run tests.
  5. Push with --force-with-lease.

Rebasing may not be the best option when:

  • multiple developers are actively sharing the same branch
  • your team prefers merge commits for branch updates
  • branch protections or repository policies do not allow rewritten history
Terminal window
git checkout feature-branch
git fetch origin
git rebase origin/main
git push --force-with-lease origin feature-branch

The pull request remains open. Once the updated branch is pushed, the PR automatically reflects the rebased commits and updated diff against the latest origin/main.

Branch names should clearly describe the purpose of the work.

Recommended patterns include:

  • feat/short-description
  • fix/short-description
  • chore/short-description
  • docs/short-description

Examples:

  • feat/add-permit-search
  • fix/handle-null-api-response
  • chore/update-dependencies
  • docs/update-onboarding-guide

Use lowercase letters and hyphens for readability and consistency.

All changes to shared branches should be introduced through pull requests.

Pull requests should:

  • Clearly describe what changed
  • Explain why the change was made
  • Reference related issues or tickets when applicable
  • Remain focused in scope
  • Be reviewed before merging
  • Pass all required automated checks

Pull requests are the primary checkpoint for collaboration, validation, and quality control.

For detailed expectations regarding pull request size, reviewer responsibilities, and approval requirements, see the code review standards.

Repositories should use automated workflows to validate changes before merging.

Typical checks include:

  • Linting and formatting checks
  • Unit and integration tests
  • Build verification
  • Static code analysis and security scans

A pull request should not be merged until required checks have completed successfully.

Branches should be kept short-lived to reduce merge conflicts and simplify review.

To support this:

  • Start new work from the latest main
  • Merge frequently
  • Avoid bundling unrelated changes together
  • Break large efforts into smaller pull requests when possible

Long-lived branches increase the risk of drift, review complexity, and integration problems.

The following practices should be avoided:

  • Direct commits to main
  • Long-lived feature branches
  • Large pull requests with unrelated changes
  • Skipping pull request review
  • Merging with failing automated checks
  • Reusing old branches for new work