DEV Community

Cover image for Our Git Workflow for Client Projects: Branching Strategy for Agencies
Michelle Turner
Michelle Turner

Posted on

Our Git Workflow for Client Projects: Branching Strategy for Agencies

How we evolved from Git chaos to a workflow that actually works for agency client projects

"Which branch has the latest changes?"

It was 4 PM on a Friday, and we had a client demo scheduled for Monday. Three developers had been working on different features, and nobody was quite sure which branch contained what. We had feature/new-dashboard, client-feedback-updates, johns-branch, fix-urgent-bug, and staging all containing different versions of the codebase.

Merging everything took the entire weekend. The demo Monday morning included two features the client had specifically asked us not to include yet, was missing one they'd approved, and had a critical bug we thought we'd fixed in a different branch.

That disaster forced us to acknowledge that our Git workflow, or lack thereof, was causing serious problems. We were an agency managing multiple client projects with varying team sizes, unclear release schedules, and frequent urgent hotfixes. None of the standard Git workflows (Git Flow, GitHub Flow, GitLab Flow) quite fit our needs.

So we created one that does.

This post explains the branching strategy we landed on after years of iteration, why we made specific choices, and how it might work for other agencies managing similar challenges.

The Agency-Specific Problem

Before diving into our solution, it's worth understanding why client projects create unique Git workflow challenges:

Variable Team Sizes

Internal products typically have consistent team sizes. Agency projects might have one developer or five, depending on project phase and client needs. Your workflow needs to scale down and up gracefully.

Unclear Release Cadences

Product teams often have regular release schedules. Client projects have unpredictable deployment windows: "We need this live before the trade show next week," or "Hold all changes until after the audit in two months."

Frequent Client Feedback

Client review cycles create complexity. You build features, deploy them to staging for client review, receive feedback, make changes, and redeploy—all while building new features in parallel.

Emergency Hotfixes

Client emergencies don't wait for sprint planning. "The contact form is broken and we're losing leads" requires immediate fixes deployed to production without disrupting ongoing feature development.

Multiple Environments

Most client projects have at least three environments (development, staging, production), and many have more (client demo environments, testing environments, etc.). Your branching strategy needs to map sensibly to these environments.

Developer Handoffs

Projects move between developers as priorities shift. Clear branching conventions prevent confusion during handoffs.

Client Access

Some clients want direct repository access or to see code. Your commit messages and branch names need to be professional and client-appropriate.

Our Branching Strategy

After trying various approaches, here's what we've settled on:

Core Branches

We maintain three permanent branches:

main - Production code

  • Contains only code that's currently in production
  • Protected—requires pull request approval to merge
  • Tagged with version numbers for each deployment
  • Never commit directly to this branch

staging - Client review environment

  • Contains all features ready for client review
  • Maps to the staging environment URL clients access
  • Features merge here for review before production
  • Can be reset or rebased if needed (but carefully)

development - Integration branch

  • Where all feature branches merge first
  • The working branch for the development team
  • May contain code that's not yet ready for client review
  • Maps to internal development environment

Feature Branches

All actual work happens in feature branches:

feature/user-authentication
feature/dashboard-redesign
feature/email-notifications
bugfix/login-form-validation
hotfix/contact-form-not-sending
Enter fullscreen mode Exit fullscreen mode

Naming conventions:

feature/[short-description]  - New features
bugfix/[short-description]   - Bug fixes during development
hotfix/[short-description]   - Urgent production fixes
chore/[short-description]    - Refactoring, dependency updates, etc.
Enter fullscreen mode Exit fullscreen mode

The Complete Flow

Here's how code moves through our workflow:

1. Developer creates feature branch from development
   git checkout development
   git pull origin development
   git checkout -b feature/user-dashboard

2. Developer works on the feature
   [make changes]
   git add .
   git commit -m "Add user statistics to dashboard"
   git push origin feature/user-dashboard

3. When ready for review, merge to development
   [create pull request]
   [code review]
   [merge to development]

4. When ready for client review, merge development to staging
   git checkout staging
   git merge development
   git push origin staging
   [deploy to staging environment]

5. After client approval, merge staging to main
   git checkout main
   git merge staging
   git tag v1.2.0
   git push origin main --tags
   [deploy to production]
Enter fullscreen mode Exit fullscreen mode

Why This Works for Us

This might seem like standard branching strategy, but the key is how we use it:

Staging as a Stable Client Demo Environment

The staging branch represents "what the client can currently see." We don't merge anything to staging until it's ready for client eyes.

This means:

  • Staging is always in a demo-able state
  • Clients have a consistent URL for reviewing work
  • We can tell clients "check staging" without worrying what they'll find

Development as Team Integration Point

The development branch lets the team integrate work without exposing incomplete features to clients. We can merge three partially complete features to development for team review while only promoting completed features to staging.

Hotfix Process

When production issues need immediate fixes:

1. Create hotfix branch from main (not development)
   git checkout main
   git pull origin main
   git checkout -b hotfix/contact-form-fix

2. Fix the issue
   [make fix]
   git commit -m "Fix contact form submission validation"

3. Merge to main and deploy
   git checkout main
   git merge hotfix/contact-form-fix
   git push origin main
   [deploy to production]

4. Backport fix to other branches
   git checkout staging
   git merge main
   git checkout development
   git merge main
Enter fullscreen mode Exit fullscreen mode

This ensures production fixes don't get lost and propagate to all branches.

Client Feedback Loop

When clients request changes to features on staging:

1. Create feedback branch from staging
   git checkout staging
   git checkout -b feature/dashboard-client-feedback

2. Make requested changes
   [implement feedback]
   git commit -m "Update dashboard colors per client feedback"

3. Merge back to staging for re-review
   git checkout staging
   git merge feature/dashboard-client-feedback
   [deploy to staging]

4. After approval, continue to main as normal
Enter fullscreen mode Exit fullscreen mode

This keeps feedback changes organized and traceable.

Practical Rules and Guidelines

These conventions make our workflow function smoothly:

Rule 1: Feature Branches Are Short-Lived

Feature branches should exist for days, not weeks. Long-lived feature branches create merge nightmares.

When features take longer:

  • Break large features into smaller, independently mergeable pieces
  • Merge to development frequently, even if the feature isn't complete
  • Use feature flags to hide incomplete functionality in staging/production

Rule 2: Never Force Push to Shared Branches

Force pushing to main, staging, or development rewrites team history and causes chaos. The only exception is staging when carefully coordinated, since it's sometimes necessary to reset it to match production after hotfixes.

Rule 3: Pull Before You Push

Always pull the latest changes before pushing your work:

git checkout development
git pull origin development
git checkout feature/my-feature
git merge development
git push origin feature/my-feature
Enter fullscreen mode Exit fullscreen mode

This catches conflicts on your machine rather than on the server.

Rule 4: Write Meaningful Commit Messages

Clients sometimes read commit history. Write commits as if they will:

Bad:

"fix stuff"
"asdf"
"more changes"
Enter fullscreen mode Exit fullscreen mode

Good:

"Add user authentication to dashboard"
"Fix validation error on contact form"
"Update API integration per client requirements"
Enter fullscreen mode Exit fullscreen mode

Rule 5: Clean Up Merged Branches

Delete feature branches after they're merged. This keeps the repository clean and prevents confusion about which branches are active.

# Delete local branch
git branch -d feature/completed-feature

# Delete remote branch
git push origin --delete feature/completed-feature
Enter fullscreen mode Exit fullscreen mode

Rule 6: Tag Production Releases

Every production deployment gets a version tag:

git tag -a v1.2.0 -m "Release version 1.2.0 - Dashboard redesign"
git push origin v1.2.0
Enter fullscreen mode Exit fullscreen mode

This makes it easy to roll back if needed and creates clear release history.

Common Scenarios and How We Handle Them

Scenario 1: Multiple Features in Development

Three developers are building different features simultaneously:

Developer A works on user authentication:

git checkout -b feature/user-auth
[work, commit, push]
Enter fullscreen mode Exit fullscreen mode

Developer B works on dashboard redesign:

git checkout -b feature/dashboard-redesign
[work, commit, push]
Enter fullscreen mode Exit fullscreen mode

Developer C works on email notifications:

git checkout -b feature/email-notifications
[work, commit, push]
Enter fullscreen mode Exit fullscreen mode

All three merge to development as they complete work. Only when all three are ready do they merge development to staging for client review.

Scenario 2: Deploying One Feature Without Others

The client approves the dashboard redesign but wants changes to authentication before approving it.

Option 1 - Cherry-pick specific commits:

git checkout staging
git cherry-pick [dashboard-commits]
Enter fullscreen mode Exit fullscreen mode

Option 2 - Create a release branch:

git checkout -b release/dashboard-only main
git merge feature/dashboard-redesign
git checkout main
git merge release/dashboard-only
Enter fullscreen mode Exit fullscreen mode

Option 3 - Use feature flags:

// In code
if (featureFlags.newAuthentication) {
  // Show new auth
} else {
  // Show old auth
}
Enter fullscreen mode Exit fullscreen mode

We prefer Option 3 for complex scenarios because it's cleaner and less error-prone.

Scenario 3: Client Finds a Bug on Staging

Client reports an issue on staging while new features are being developed:

# Create bugfix from staging (not development)
git checkout staging
git checkout -b bugfix/staging-issue

# Fix and test
[make fix]
git commit -m "Fix broken link on contact page"

# Merge to staging
git checkout staging
git merge bugfix/staging-issue
[deploy to staging for client verification]

# After verification, merge to main
git checkout main  
git merge staging

# Backport to development
git checkout development
git merge main
Enter fullscreen mode Exit fullscreen mode

Scenario 4: Urgent Production Hotfix

Production issue discovered that needs immediate fix:

# Start from main (current production code)
git checkout main
git pull origin main
git checkout -b hotfix/critical-security-issue

# Fix, test locally
[implement fix]
git commit -m "Patch security vulnerability in user input handling"

# Deploy directly to main
git checkout main
git merge hotfix/critical-security-issue
git tag v1.2.1
git push origin main --tags
[emergency deploy to production]

# Backport to other branches
git checkout staging
git merge main
git checkout development
git merge main
Enter fullscreen mode Exit fullscreen mode

Scenario 5: Starting a New Project Phase

Client approves everything on staging and it's all deployed to production. Now starting work on Phase 2:

# Ensure all branches are synchronized
git checkout main
git pull origin main

git checkout staging
git merge main
git push origin staging

git checkout development
git merge main
git push origin development

# Clean slate for new phase
# All three branches now contain the same code
Enter fullscreen mode Exit fullscreen mode

Tooling and Automation

We use several tools to make this workflow more manageable:

Pull Request Templates

GitHub/GitLab PR templates ensure consistent information:

## Description
[Brief description of changes]

## Type of Change
- [ ] New feature
- [ ] Bug fix
- [ ] Hotfix
- [ ] Refactoring

## Client Impact
- [ ] Ready for client review on staging
- [ ] Internal only (not yet client-facing)

## Testing
- [ ] Tested locally
- [ ] Tested on development environment

## Checklist
- [ ] Code follows project style guidelines
- [ ] Commit messages are clear and professional
- [ ] Documentation updated if needed
Enter fullscreen mode Exit fullscreen mode

Branch Protection Rules

We configure repository settings to enforce workflow:

Main branch:

  • Require pull request reviews
  • Require status checks to pass
  • Prevent force pushes
  • Prevent deletions

Staging branch:

  • Require status checks to pass
  • Prevent force pushes (with exceptions for leads)

Development branch:

  • Require status checks to pass

Automated Deployments

We use CI/CD to automatically deploy:

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - main
      - staging
      - development

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Deploy to environment
        run: |
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            ./deploy.sh production
          elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
            ./deploy.sh staging
          elif [ "${{ github.ref }}" == "refs/heads/development" ]; then
            ./deploy.sh development
          fi
Enter fullscreen mode Exit fullscreen mode

This ensures branches and environments stay synchronized automatically.

Git Aliases

We create aliases for common operations:

# ~/.gitconfig
[alias]
    # Update branch with latest from origin
    sync = !git fetch origin && git rebase origin/$(git branch --show-current)

    # Create feature branch from development
    feature = !sh -c 'git checkout development && git pull && git checkout -b feature/$1' -

    # Create bugfix branch
    bugfix = !sh -c 'git checkout development && git pull && git checkout -b bugfix/$1' -

    # Create hotfix from main
    hotfix = !sh -c 'git checkout main && git pull && git checkout -b hotfix/$1' -

    # Clean up merged branches
    cleanup = !git branch --merged | grep -v \"\\*\\|main\\|staging\\|development\" | xargs -n 1 git branch -d
Enter fullscreen mode Exit fullscreen mode

What We Learned the Hard Way

These lessons came from mistakes:

Lesson 1: Don't Skip Development Branch

We initially tried using just staging and main, merging features directly to staging. This created problems when we wanted to integrate features for team review before client review.

The development branch isn't overhead—it's essential for team coordination.

Lesson 2: Staging Can Be Reset, But Document It

Sometimes staging gets messy—perhaps it has experimental features that won't move forward, or hotfixes have made main ahead of staging.

It's okay to reset staging to match main:

git checkout staging
git reset --hard main
git push --force origin staging
Enter fullscreen mode Exit fullscreen mode

But communicate this to the team first, and make sure nobody has work in staging that needs to be preserved.

Lesson 3: Feature Flags Beat Complex Branching

When we need to deploy some features but not others, feature flags are cleaner than complex cherry-picking or release branch strategies.

Lesson 4: Commit Messages Matter More Than You Think

We learned this when a client asked to review the Git history to see what had been implemented. Sloppy commit messages made us look unprofessional.

Lesson 5: Merge Don't Rebase for Shared Branches

Rebasing rewrites history and causes problems for anyone else who's pulled the branch. Use merge for branches others might have checked out.

Reserve rebasing for cleaning up personal feature branches before merging them.

Adapting This Workflow

This workflow works for Nuvoro Digital, but you should adapt it to your needs:

For smaller projects: You might not need the development branch. Merge features directly to staging, then to main.

For larger teams: You might need more structure—perhaps requiring code review for development merges, not just main merges.

For different environments: If you have QA environments, demo environments, etc., add corresponding branches or use feature flags to control what's visible where.

For continuous deployment: If you deploy to production multiple times daily, you might simplify to just main and feature branches, using feature flags for control.

The key principles to preserve:

  • Clear mapping between branches and environments
  • Short-lived feature branches
  • Protection of production code
  • Ability to hotfix production quickly
  • Professional commit history

The Bottom Line

Git workflow isn't about following a textbook pattern—it's about creating a system that supports how your team actually works.

For agencies managing client projects, that means:

  • Supporting unpredictable deployment schedules
  • Maintaining stable client demo environments
  • Enabling fast hotfixes without disrupting feature work
  • Scaling from solo developers to larger teams
  • Keeping history professional and trackable

Our three-branch workflow with feature branches accomplishes this. It's not the simplest possible workflow, but it's the right amount of structure for the complexity we manage.

That Friday afternoon when we couldn't figure out which branch had what? Hasn't happened since we adopted this workflow. Everyone knows where code goes, how it moves between branches, and where to find the latest version of anything.

Sometimes the best workflow isn't the trendy one or the theoretically elegant one—it's the one that prevents disasters and lets your team focus on building great software instead of fighting Git.


What Git workflow does your team use for client projects? Have you found a better approach for managing the agency-specific challenges we've described? Share your experiences in the comments, we're always looking to improve.

Top comments (1)

Collapse
 
kamalmost profile image
KamalMostafa

THANKS so much ❤️ for sharing...logged in just say that. keep them coming 👍