Blog post

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: and fix typo commits mixed into main make it impossible to understand what changed between releases.
  • Merge conflicts at the worst time - long-lived branches that drift far from main produce 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:

  1. main is always shippable. Any commit on main must build and pass all tests. There are no exceptions. This means you can release at any moment without a stabilisation period.
  2. All work happens on branches. No matter how small the change, it goes on a feature/ or hotfix/ branch first. This keeps main clean and gives you a natural review checkpoint before anything lands.
  3. History on main is intentional. We use squash merges so every commit on main represents one complete, meaningful unit of work with a descriptive message. The messy in-progress commits stay on feature branches where they belong.
  4. 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.

main feature/A feature/B always deployable merge merge

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
  • main must 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
git pull git checkout -b main 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
GitHub (remote backup) push wip: fix: wip: style: fix: main 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-lease is safer than --force - it refuses to push if someone else has pushed to the same branch since your last fetch.
Before rebase After rebase new commits on main feature (out of date) main feature rebase up to date main feature

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
wip: fix: wip: style: fix: wip: feature/add-pressure-units --squash feat: add pressure unit category main

--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.
feat: add pressure unit category type short description
Type When to use
featNew feature or capability
fixBug fix
choreVersion bump, dependency update, config change
refactorCode change with no behavior change
styleCSS, formatting, no logic change
docsDocumentation 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.

  1. Ensure main is stable and all intended features are merged
  2. Bump the version number in two files:
    • package.json - "version" field
    • tauri.conf.json - "version" field
    • Both must match exactly
  3. 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
  4. 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
  5. Review the draft release on GitHub, add release notes, then publish it
GitHub Actions CI v1.0.1 chore: bump version main

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
v1.0.2 main hotfix/description-of-bug fix: 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:

  1. main is always shippable. Only deliberate, reviewed releases reach main. Because every merge goes through a PR on develop first, the bar for what lands on main is even higher than in Approach 1.
  2. All work happens on branches. Every unit of work - feature, fix, or refactor - lives on a short-lived feature/* branch. Nobody commits directly to develop or main.
  3. Code review is mandatory before integration. Unlike Approach 1, no feature lands on develop without a Pull Request that is reviewed and approved by at least one other team member. This is the defining constraint of this workflow.
  4. develop is the integration truth. develop is a permanent branch that always reflects the latest integrated work. It must remain buildable at all times - treat it with the same care as main.
  5. Releases are explicit promotions. Merging develop into main is a conscious act, not a side effect of daily work. Each such merge represents a versioned release and is tagged accordingly.
feature/new-task PR merge develop release PR v1.0.0 main
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
git pull git checkout -b develop 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
GitHub - Pull Request push feat: fix: style: fix: approved PR merge feature/your-task-name develop

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
GitHub Actions CI release PR v1.0.0 develop main

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
sync fix v1.0.1 hotfix/description-of-bug develop main
Always sync hotfixes back into develop. If you skip this step, the fix will be overwritten the next time a release PR from develop reaches main.

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