Team Workflows
Before you start
You need: pull requests (Article 05).
Time: about 45 minutes.
Learning objective
Understand the common branching workflows, know which one a team is using from its branch list, and follow it correctly.
Topics
- Why a workflow is needed
- GitHub Flow
- Git Flow
- Trunk-based development
- Choosing between them
- Hotfixes
- Tags and releases
- Working within someone else's workflow
Why a workflow is needed
With one developer, any approach works. With six, unstated rules produce:
- Commits going straight to
main - Nobody knowing which branch is deployed
- A hotfix that cannot be applied because the release branch has moved on
- Two people rewriting the same file
- A release containing an unfinished feature
A workflow is an agreement about where work starts, where it merges and what is deployable. The specific workflow matters far less than everyone following the same one.
GitHub Flow
The simplest workflow that works for a team.
main ─────●───────●───────●───────●──── (always deployable)
\ / \ /
●───● ●───●
feature/search bugfix/fee-rounding
Rules:
mainis always deployable.- Branch from
mainfor anything. - Push and open a pull request.
- Review and CI.
- Merge to
main. - Deploy.
| Advantage | Cost |
|---|---|
| Easy to explain | No place for a long stabilisation period |
| One long-lived branch | Requires strong automated tests |
| Fast to production | Requires the ability to deploy on demand |
Suits continuous deployment, web applications, and small-to-medium teams. It is the default for most modern projects and the right starting point unless something specific rules it out.
It depends entirely on main staying deployable, which is why branch protection and CI are not optional here.
Git Flow
Older, more structured, built around scheduled releases.
main ─────●───────────────●────── (production, tagged)
\ /
release ─●───●───────●
/ \
develop ────●───●───●───●───●───●──── (integration)
\ / \ /
●─● ●─●
feature/x feature/y
| Branch | Purpose | Lifetime |
|---|---|---|
main | Production; every commit is a tagged release | Permanent |
develop | Integration of finished features | Permanent |
feature/* | One feature; branches from and merges to develop | Days to weeks |
release/* | Stabilisation; bug fixes only, no new features | Days |
hotfix/* | Urgent production fix; branches from main | Hours |
A release:
git switch -c release/2.4.0 develop
# only bug fixes here
git switch main
git merge --no-ff release/2.4.0
git tag -a v2.4.0 -m "Release 2.4.0"
git switch develop
git merge --no-ff release/2.4.0 # fixes go back to develop
git branch -d release/2.4.0
The merge back into develop is the step teams forget, and the result is a bug fixed during the release reappearing in the next one.
| Advantage | Cost |
|---|---|
| Explicit stabilisation window | Many long-lived branches |
| Parallel release and development | Frequent, large merges |
| Clear hotfix path | Slow; complex to explain |
Suits versioned or on-premise software — installed products, mobile apps with store review, anything with a scheduled release. It is heavy for a continuously deployed web application, and using it there is a common mistake.
Trunk-based development
Everyone commits to main, with branches lasting hours.
main ──●──●──●──●──●──●──●──●──●──●── (many small commits daily)
Rules:
- Branches live less than a day, often less than an hour.
- Merge to
mainat least daily. - Incomplete work hides behind a feature flag.
- Comprehensive automated tests.
- Deployment is automated.
if (_featureFlags.IsEnabled("StudentSearch", schoolId))
{
return await _searchService.SearchStudentsAsync(schoolId, query);
}
return await _studentService.GetAllStudentsAsync(schoolId);
Feature flags are what make this possible. Code ships to production disabled, gets enabled for one school, then for everyone. Deployment and release become separate events — and turning a feature off is a flag change, not a rollback.
| Advantage | Cost |
|---|---|
| Almost no merge conflicts | Requires excellent test coverage |
| Continuous integration in the literal sense | Requires flag infrastructure |
| Fast feedback | Requires discipline; unmerged work is the failure mode |
Suits mature teams with strong automation. Adopted without tests it is simply "everyone commits to main", which is worse than any alternative.
Choosing
| Situation | Workflow |
|---|---|
| Small team, continuous deployment | GitHub Flow |
| Scheduled releases, multiple versions supported | Git Flow |
| Mature team, strong tests, feature flags | Trunk-based |
| Learning, or a personal project | GitHub Flow |
| Regulated release process | Git Flow |
Start with GitHub Flow. Move to Git Flow if you genuinely need release branches, or trunk-based when your automation can support it. Choosing Git Flow because it looks professional is the most common wrong answer.
The workflows are also not exclusive — many teams run GitHub Flow with a release/* branch when a specific release needs stabilising.
Hotfixes
Production is broken; a fix must ship without shipping whatever else is on develop.
git switch -c hotfix/login-500 main # branch from what is in production
# fix, test
git switch main
git merge --no-ff hotfix/login-500
git tag -a v2.3.1 -m "Hotfix: login returns 500 for staff accounts"
git push origin main --tags
# deploy
git switch develop
git merge --no-ff hotfix/login-500 # do not skip this
git branch -d hotfix/login-500
Branch from main, not develop. Branching from develop drags in every unreleased change, so a one-line fix becomes an unplanned release.
Merge back into develop. Otherwise the next release reintroduces the bug you just fixed — a genuinely common and confusing failure.
Under pressure, both rules are the ones that get skipped. Write them into the runbook.
Tags and releases
git tag -a v2.4.0 -m "Release 2.4.0"
git tag -a v2.4.0 a3f9c1d -m "..." # tag an older commit
git push origin v2.4.0
git push origin --tags
git tag -l "v2.4.*"
git show v2.4.0
git checkout v2.4.0 # detached HEAD; expected
Use annotated tags (-a), which store the tagger, date and message as real objects. Lightweight tags are just a name on a hash and carry no record of who released what.
Tags are not pushed by git push. They need --tags or an explicit push — a frequent surprise.
Semantic versioning:
v2.4.1
│ │ │
│ │ └── PATCH — backwards-compatible fix
│ └──── MINOR — backwards-compatible feature
└────── MAJOR — breaking change
A tag is how you answer "what exactly is in production?" Without tags, that question requires archaeology through commit dates and deployment logs.
Working within someone else's workflow
Joining a team, read the branches first:
git branch -a
git log --oneline --graph --all -30
git tag -l
| What you see | Likely workflow |
|---|---|
Only main plus short-lived branches | GitHub Flow |
main and develop both active | Git Flow |
release/* branches | Git Flow |
Almost everything on main, many small commits | Trunk-based |
Tags like v2.4.0 | Versioned releases |
Then ask four questions:
- Which branch do I branch from?
- Which branch do I target in a pull request?
- What is deployed to production right now?
- How is a hotfix handled?
Read CONTRIBUTING.md before asking — most teams have written this down.
Follow the existing workflow even if you prefer another. A team half on one workflow and half on another is worse than either.
Diagnosing
| Symptom | Cause |
|---|---|
| A fixed bug reappears next release | Hotfix or release branch never merged back |
| A hotfix pulls in unrelated features | Branched from develop instead of main |
| Nobody knows what is in production | No tags |
| Constant conflicts | Long-lived branches — merge from main more often |
| An unfinished feature shipped | No release branch and no feature flag |
| Tags missing on the remote | Pushed without --tags |
Errors you will hit
| What you see | Cause | Fix |
|---|---|---|
| A fixed bug reappears next release | Hotfix never merged back | Merge it into the development branch |
| A hotfix drags in unreleased work | Branched from the wrong place | Branch from what is in production |
| Nobody knows what is in production | No tags | Tag every release |
| Constant conflicts | Long-lived branches | Merge from the main branch daily |
| Tags missing on the remote | Pushed without --tags | git push origin --tags |
Both hotfix rules get skipped under pressure, which is exactly when they matter. Write them into the runbook.
Common mistakes
- Adopting Git Flow for a continuously deployed web application
- Trunk-based development without test coverage
- Hotfix branched from
develop - Not merging a hotfix or release branch back
- Lightweight tags instead of annotated
- Forgetting to push tags
- Feature branches alive for weeks
- Mixing workflows within one team
- Committing directly to
mainwhere the workflow forbids it
Practice
The course exercise is follow a branching workflow.
- Set up a repository and run GitHub Flow: two feature branches, two PRs, both merged to
main. - Set up a second repository with
mainanddevelop. Run a full Git Flow release, including the merge back intodevelop. - Deliberately skip the merge back into
develop, then start the next release and find the reintroduced bug. - Branch a hotfix from
developand observe what it drags in. Redo it frommain. - Tag a release with
-a, push it with--tags, and confirm it appears on GitHub. - Create a lightweight tag and compare
git showoutput with the annotated one. - Check out a tag and note the detached HEAD message.
- Implement a feature flag around an unfinished feature and merge it to
maindisabled. - Simulate trunk-based development: three commits a day for three days, all to
main, nothing living longer than a day. - Open an unfamiliar public repository and identify its workflow from
git branch -aand the graph. - Write a
CONTRIBUTING.mdanswering the four questions for your own project. - Keep a branch unmerged for a week while
mainmoves, then merge it. Note the conflict count.
You can now
- Identify a team's workflow from its branches
- Follow it correctly, including for a hotfix
- Branch a hotfix from production, and merge it back
- Tag releases and push the tags
- Choose a workflow that fits the team
Review questions
- What does GitHub Flow require in order to be safe?
- Why must a hotfix branch from
mainrather thandevelop? - What makes trunk-based development possible without shipping unfinished features?
- Why are annotated tags preferred, and what stops them reaching the remote?
Next: Intermediate Git