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
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.
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]
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
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
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
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"
Good:
"Add user authentication to dashboard"
"Fix validation error on contact form"
"Update API integration per client requirements"
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
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
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]
Developer B works on dashboard redesign:
git checkout -b feature/dashboard-redesign
[work, commit, push]
Developer C works on email notifications:
git checkout -b feature/email-notifications
[work, commit, push]
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]
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
Option 3 - Use feature flags:
// In code
if (featureFlags.newAuthentication) {
// Show new auth
} else {
// Show old auth
}
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
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
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
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
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
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
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
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)
THANKS so much ❤️ for sharing...logged in just say that. keep them coming 👍