Development Workflow Guide
After working on several collaborative projects, some with clear conventions, I came to appreciate just how much the quality of teamwork depends on how you use version control system (VCS) like Git. Git is extraordinarily powerful, but that power works against you if everyone on the team is using it differently. Uncoordinated workflows produce broken history, avoidable merge conflicts, and the kind of late-night debugging sessions that stem entirely from process, not code.
I decided to exploit Git deliberately as the backbone of how collaborative work gets organized, reviewed, and shipped. The result is this guide: a set of conventions that channels Git's flexibility into a predictable, low-friction workflow with two approaches. It covers branching, committing, rebasing, merging, and releasing. Everyone on the project is expected to follow this workflow consistently.
Why a Defined Workflow Matters
Without a shared workflow, version control becomes a source of friction rather than a safety net. Common problems that arise on teams without clear conventions include:
- Broken
main- direct commits or untested merges leave the branch in a state that cannot be shipped. - Tangled history - dozens of
wip:andfix typocommits mixed intomainmake it impossible to understand what changed between releases. - Merge conflicts at the worst time - long-lived branches that drift far from
mainproduce large, painful conflicts exactly when you are trying to ship. - Lost work - branches that only exist locally have no backup; a single hardware failure can erase days of progress.
- Unclear releases - without a tagging convention, it is difficult to know what code is running in production or how to reproduce a previous release.
These workflows are designed to eliminate each of these problems through a small set of rules that are easy to follow once they become habit.
Approach 1: Trunk-Based Strategy
This is the simplest workflow and the one I reach for first on small teams.
There is only one permanent branch - main - and all work flows
through short-lived feature branches that are squash-merged and deleted.
No pull requests, no integration layers: just a clean trunk that is always
ready to ship.
Core Principles
Every decision in this approach follows from four principles:
mainis always shippable. Any commit onmainmust build and pass all tests. There are no exceptions. This means you can release at any moment without a stabilisation period.- All work happens on branches. No matter how small the change, it goes on a
feature/orhotfix/branch first. This keepsmainclean and gives you a natural review checkpoint before anything lands. - History on
mainis intentional. We use squash merges so every commit onmainrepresents one complete, meaningful unit of work with a descriptive message. The messy in-progress commits stay on feature branches where they belong. - Branches are short-lived. A branch that stays open for weeks accumulates drift and merge debt. Aim to merge and delete within days, not weeks.
Repository Overview
This workflow uses a lightweight branch strategy optimized for small teams.
The goal is to keep main branch always releasable while allowing
parallel work without interference.
Branches
There are only two types of branches in this workflow:
| Branch | Purpose | Who pushes here |
|---|---|---|
main |
Production-ready code. Every commit here is a potential release. | Via merge from feature branches only |
feature/* |
Any unit of work - new feature, bug fix, refactor, experiment | The developer who owns the task |
Rules:
- Never commit directly to
main mainmust always build successfully- Delete feature branches after merging
Starting a Task
Always branch from the latest main:
git checkout main
git pull origin main
git checkout -b feature/your-task-name
Naming convention - use lowercase, hyphens, no spaces:
feature/add-pressure-units
feature/fix-swap-button-crash
feature/update-installer-ui
feature/refactor-conversion-engine
If it's a critical production fix, prefix with hotfix/:
hotfix/crash-on-missing-config
Working on Your Branch
Commit as often as you like. Commit messages on feature branches can be informal - they will be cleaned up at merge time.
git add .
git commit -m "wip: pressure base units"
git commit -m "fix: wrong pascal factor"
git push origin feature/add-pressure-units
Push your branch to GitHub regularly. This acts as a remote backup and lets others see work in progress.
Keeping Your Branch Up to Date
If main has moved forward while you were working, rebase your
branch onto it to avoid conflicts at merge time:
git fetch origin
git rebase origin/main
Resolve any conflicts, then continue:
git rebase --continue
git push origin feature/add-pressure-units --force-with-lease
--force-with-leaseis safer than--force- it refuses to push if someone else has pushed to the same branch since your last fetch.
Merging Completed Work into Main
When your task is done and tested locally:
git checkout main
git pull origin main
git merge --squash feature/add-pressure-units
git commit -m "feat: add pressure unit category"
git push origin main
git branch -d feature/add-pressure-units
git push origin --delete feature/add-pressure-units
--squash collapses all your WIP commits into one clean commit on
main. Write a meaningful final commit message at this point.
If the task was done by someone else, review their branch locally before merging:
git fetch origin
git checkout feature/add-pressure-units
# test it
git checkout main
git merge --squash feature/add-pressure-units
Commit Message Format
Use this format for all commits that land on main:
<type>: <short description>
Optional longer explanation if needed.
| Type | When to use |
|---|---|
feat | New feature or capability |
fix | Bug fix |
chore | Version bump, dependency update, config change |
refactor | Code change with no behavior change |
style | CSS, formatting, no logic change |
docs | Documentation only |
Examples:
feat: add pressure unit category
fix: swap button does not clear filters
chore: bump version to 1.0.1
refactor: extract conversion logic into separate module
Making a Release
Only the release owner (typically the lead developer) performs this step.
- Ensure
mainis stable and all intended features are merged -
Bump the version number in two files:
package.json-"version"fieldtauri.conf.json-"version"field- Both must match exactly
-
Commit and tag:
git commit -am "chore: bump version to 1.0.1" git push origin main git tag v1.0.1 git push origin v1.0.1 -
The GitHub Actions CI pipeline triggers automatically on the tag push. It will:
- Build the Windows installer
- Sign the update package
- Create a draft GitHub Release with all artifacts
- Update the auto-updater manifest
- Review the draft release on GitHub, add release notes, then publish it
Hotfixes
For critical bugs that must go to production immediately, bypassing normal development:
git checkout main
git pull origin main
git checkout -b hotfix/description-of-bug
# fix the bug, test it
git checkout main
git merge --squash hotfix/description-of-bug
git commit -m "fix: description of bug"
git push origin main
# release immediately
git tag v1.0.2
git push origin v1.0.2
# clean up
git branch -d hotfix/description-of-bug
git push origin --delete hotfix/description-of-bug
Rules Summary
| ✓ Do | ✗ Don't |
|---|---|
Branch from main for every task |
Commit directly to main |
| Push your branch to GitHub regularly | Leave branches open indefinitely after merging |
Rebase onto main before merging if it has moved |
Merge main into your feature branch (use rebase instead) |
| Write a clean squash commit message | Leave "wip:" messages on main |
| Test locally before merging | Merge broken code assuming CI will catch it |
| Delete branches after merging | Accumulate stale branches |
Approach 2: Integration Branch Strategy
This approach introduces a dedicated develop branch as a
permanent integration layer between feature work and production. Features
are merged into develop via Pull Requests - enabling mandatory
code review before integration - and only promoted to main
when a deliberate release decision is made.
This model suits teams that require a peer-review gate before integration, or projects under compliance requirements where an audit trail of PRs is valuable.
Core Principles
This approach shares the same foundations as Approach 1, with two important distinctions driven by the extra integration layer:
mainis always shippable. Only deliberate, reviewed releases reachmain. Because every merge goes through a PR ondevelopfirst, the bar for what lands onmainis even higher than in Approach 1.- All work happens on branches. Every unit of work - feature, fix, or refactor - lives on a short-lived
feature/*branch. Nobody commits directly todevelopormain. - Code review is mandatory before integration. Unlike Approach 1, no feature lands on
developwithout a Pull Request that is reviewed and approved by at least one other team member. This is the defining constraint of this workflow. developis the integration truth.developis a permanent branch that always reflects the latest integrated work. It must remain buildable at all times - treat it with the same care asmain.- Releases are explicit promotions. Merging
developintomainis a conscious act, not a side effect of daily work. Each such merge represents a versioned release and is tagged accordingly.
| Branch | Purpose | Direct commits? | CI trigger |
|---|---|---|---|
main |
Production-ready code only. Every commit is a released version. | Never | Tag push → build + release |
develop |
Integration layer - always buildable, accepts feature PRs | Minor fixes only | Optional: test build on push |
feature/* |
One feature or bug fix | Yes | Nothing |
Starting a New Feature
Always branch from the latest develop, never from main:
git checkout develop
git pull origin develop
git checkout -b feature/your-task-name
Finishing a Feature
When your feature is complete, push the branch to GitHub and open a
Pull Request targeting develop. A team member reviews the
code before it is merged.
# push your work
git push origin feature/your-task-name
# open a PR on GitHub:
# base: develop <- compare: feature/your-task-name
After the PR is merged on GitHub, update your local repository and delete the feature branch:
git checkout develop
git pull origin develop
git branch -d feature/your-task-name
git push origin --delete feature/your-task-name
Making a Release
When develop is stable and contains everything intended for
the next release, open a Pull Request from develop into
main. After it is merged, tag the commit on main
to trigger the CI pipeline.
# open a PR on GitHub:
# base: main <- compare: develop
# after the PR is merged locally:
git checkout main
git pull origin main
git tag v1.0.0
git push origin v1.0.0
Hotfixes
For critical bugs on production, branch directly from main.
After fixing, merge back into both main
(to ship the fix) and develop (to keep the branches in sync).
# branch from main
git checkout main
git pull origin main
git checkout -b hotfix/description-of-bug
# fix the bug, commit
git add .
git commit -m "fix: description of bug"
# merge into main, tag and release
git checkout main
git merge --squash hotfix/description-of-bug
git commit -m "fix: description of bug"
git push origin main
git tag v1.0.1
git push origin v1.0.1
# back-sync the fix into develop
git checkout develop
git merge main
git push origin develop
# clean up
git branch -d hotfix/description-of-bug
git push origin --delete hotfix/description-of-bug
Always sync hotfixes back intodevelop. If you skip this step, the fix will be overwritten the next time a release PR fromdevelopreachesmain.
Branch Protection Rules
Configure these settings in GitHub → Settings → Branches to enforce the workflow automatically.
For main:
- Require a pull request before merging
- Require at least 1 approval
- Require status checks to pass (CI build)
- Do not allow force pushes
- Do not allow direct pushes
For develop:
- Require a pull request before merging
- Require at least 1 approval
- Optionally require status checks
- Do not allow force pushes
Rules Summary
| ✓ Do | ✗ Don't |
|---|---|
Branch from develop for every feature |
Branch feature work from main |
Open a PR to merge features into develop |
Merge directly to develop without a review |
Open a release PR from develop into main |
Push directly to main |
Sync hotfixes back into develop |
Skip back-merging hotfixes - they will be overwritten |
| Delete feature branches after merging | Leave stale branches open |
Comparing the Two Approaches
Both workflows share the same goal - keeping main
production-ready at all times - but they make different trade-offs
between simplicity and safety.
| Aspect | Approach 1 - Trunk-Based | Approach 2 - Integration Branch |
|---|---|---|
| Branch structure | main + feature/* |
main + develop + feature/* |
| Merge strategy | Squash merge locally; no PR required | PR into develop; PR for releases to main |
| Code review | Optional, ad-hoc | Enforced via PR approval gate |
| Hotfix handling | Branch from main, merge to main only |
Branch from main, merge to both main and develop |
| Audit trail | Clean linear history on main |
PR history visible; merge commits on develop |
| Complexity | Low - fewer steps, faster iteration | Higher - more ceremony, more process control |
| Best for | Small teams, solo developers, fast-moving projects | Teams needing mandatory review, compliance requirements |
When to Choose Each
Choose Approach 1 (Trunk-Based) when:
- The team is small (1–3 developers) and communicates closely
- Speed of iteration is the top priority
- You have strong CI coverage that catches regressions automatically
- There is no external requirement for a PR-based review trail
Choose Approach 2 (Integration Branch) when:
- Code review is mandatory - compliance, security audits, or team policy
- The team is larger and coordination through PRs prevents conflicts
- You want a permanent integration record separate from production history
- Onboarding new developers who benefit from structured reviews
