LogoThreatmatic
Strategy

Branching Strategy

This document defines our Git branching model and workflow

This document defines our Git branching model and workflow.

Branch Overview

We use a simplified GitFlow model with two primary branches.

BranchPurposeProtectedDeploys To
mainProduction-ready codeYesProd
developIntegration branchYesQA
feature/*Feature developmentNo-
fix/*Bug fixes (normal priority)No-
hotfix/*Emergency fixesNo-

Branch Policy Enforcement

PRs to main are restricted to ensure release integrity. This is enforced automatically by the PR validation workflow.

Source BranchAllowed TargetUse Case
feature/*develop onlyNew functionality
fix/*develop onlyBug fixes (normal priority)
hotfix/*mainEmergency production fixes (then cherry-pick to develop)
developmainRelease merges

Automated Enforcement:

The pr-validate.yml workflow checks branch policy on every PR:

  • PRs to main from unauthorized branches (e.g., feature/*, fix/*) will fail with a clear error message
  • PRs to develop are allowed from any branch
  • Ensures only tested code reaches production via the proper flow

If you attempt to create a PR from feature/my-change to main, you'll see:

::error::PRs to main must come from 'develop' or 'hotfix/*' branches
::error::Source branch 'feature/my-change' is not allowed to target main.
::error::Please target 'develop' instead, or rename to 'hotfix/*' for emergency fixes.

Branch Flow


Branch Details

main Branch

Purpose: Represents production-ready code. Every commit to main should be deployable to production.

Rules:

  • Protected branch (no direct commits)
  • Requires pull request from develop
  • Requires at least one approval
  • All CI checks must pass

Deployment: Pushes to main trigger deployment to Production environment.


develop Branch

Purpose: Integration branch where features are combined and tested before release.

Rules:

  • Protected branch
  • Receives automated exports from Dev environment (nightly)
  • Receives pull requests from feature branches
  • Can receive direct commits from automated export pipeline

Deployment: Pushes to develop trigger deployment to QA environment.


feature/* and fix/* Branches

Purpose: Isolated development of specific features or bug fixes.

Naming:

  • feature/{short-description} - New functionality
  • fix/{short-description} - Bug fixes (normal priority)

Examples:

feature/add-account-validation
feature/new-contact-form
fix/workflow-error-handling
fix/form-validation-bug

Workflow:

  1. Create from develop
  2. Make changes in Dev environment
  3. Export and commit to feature/fix branch
  4. Create PR to develop (PRs to main will be rejected)
  5. Delete after merge

hotfix/* Branches

Purpose: Emergency fixes that need to go directly to production.

Naming: hotfix/{issue-description}

Examples:

hotfix/fix-critical-workflow
hotfix/security-patch

Workflow:

  1. Create from main
  2. Make minimal fix
  3. PR to main (for immediate production deployment)
  4. Cherry-pick or merge back to develop
  5. Delete after merge

Daily Workflow

Automated Export (Nightly)

Feature Development

Production Release


Pull Request Requirements

PR to develop

RequirementRequired?
CI pipeline passesYes
At least 1 approvalRecommended
No merge conflictsYes
Linked work itemOptional

PR to main

RequirementRequired?
CI pipeline passesYes
At least 1 approvalYes
QA sign-offYes
No merge conflictsYes
All conversations resolvedYes

Merge Strategy

We use different merge strategies for different branch flows to optimize history clarity.

Squash Merge: Feature → Develop

Use squash merge when merging feature branches into develop.

feature/add-validation (12 commits) → develop (1 squashed commit)

Why squash:

ReasonExplanation
Clean historyFeature branches have noisy commits ("WIP", "fix typo", "try again")
Atomic featuresEach feature = one commit, easy to identify and revert
SmallSolution exports create many small commits; squashing cleans this up
PR preserves detailGranular commits still visible in closed PR if needed

GitHub setting: Repository Settings → Pull Requests → Allow squash merging ✓


Regular Merge: Develop → Main

Use regular merge (merge commit) when merging develop into main.

develop → main (merge commit preserves all feature commits)

Why regular merge:

ReasonExplanation
Preserves featuresEach squashed feature commit flows through to main
Release boundariesMerge commit marks exactly when a release happened
Traceability"Prod broke" → Which release? → Which feature? → Easy to trace
Selective revertCan revert one feature without reverting entire release

GitHub setting: Repository Settings → Pull Requests → Allow merge commits ✓


Why NOT Squash Both Ways?

If you squash developmain:

❌ BAD: Squash develop to main
main:
├── Release 5 (one giant commit with 10 features mixed together)
├── Release 4 (one giant commit with 8 features)
└── Release 3 (one giant commit)

Problems:
- "Which feature broke prod?" - Can't tell, all mixed together
- "Revert just account validation" - Can't, it's mixed with other features
- Loss of audit trail
✅ GOOD: Regular merge develop to main
main:
├── Merge develop → main (Release 5)
│   ├── feat: add account validation
│   ├── feat: new contact form
│   └── fix: workflow error
├── Merge develop → main (Release 4)
│   ├── feat: dashboard updates
│   └── feat: reporting changes

Benefits:
- Clear release boundaries (merge commits)
- Feature-level granularity preserved
- Can revert specific features OR entire releases

Branch Rulesets

We use GitHub Rulesets (not legacy branch protection) to enforce different merge strategies per branch. This is critical for our workflow:

  • Squash merge only for PRs to develop (clean feature commits)
  • Merge commit only for PRs to main (preserve feature history)

Ruleset definitions are stored in .github/rulesets/ for reference.

main Branch Ruleset

RuleSettingReason
Require PRYesNo direct commits to production
Required approvals1Human review before production
Dismiss stale reviewsYesRe-review after new commits
Require last push approvalYesFinal review after any changes
Require conversation resolutionYesAll feedback must be addressed
Status checks (strict)YesBranch must be up-to-date
Required checksValidation StatusPR validation workflow
Allowed merge methodsMerge commit onlyPreserve feature commits on main
Branch deletionBlockedPrevent accidental deletion
Force pushesBlockedProtect history

Key: allowed_merge_methods: ["merge"] - Squash is NOT allowed on main.

develop Branch Ruleset

RuleSettingReason
Require PRYesFeature branches merge via PR
Required approvals1Code review
Dismiss stale reviewsYesRe-review after changes
Require conversation resolutionYesAll feedback must be addressed
Status checks (strict)NoNightly exports would conflict
Required checksValidation StatusPR validation workflow
Allowed merge methodsSquash onlyClean feature history
Branch deletionBlockedPrevent accidental deletion
Force pushesBlockedProtect history

Key: allowed_merge_methods: ["squash"] - Merge commits NOT allowed on develop.

Note: Required approvals is set to 1 (not 0) to ensure code review even for the integration branch.

Repository Merge Settings

Repository-level settings (Settings → Pull Requests) enable both methods:

  • ✅ Allow merge commits (for main)
  • ✅ Allow squash merging (for develop)
  • ❌ Allow rebase merging (disabled)

Squash commit formatting: When squash merging to develop, commits use:

  • Title: PR title (clean, descriptive feature name)
  • Message: PR body (contains context, linked issues, etc.)

This ensures squashed commits are meaningful and traceable back to their PR.

The rulesets control which method is available for each target branch.

Applying Rulesets

Recommended: Use the PowerShell script for idempotent setup (handles both create and update):

# Configure all rulesets and merge settings
.\tools\Setup-BranchProtection.ps1

# Preview changes without applying
.\tools\Setup-BranchProtection.ps1 -WhatIf

Manual API (initial creation only):

# These POST commands only work for NEW rulesets (fail if already exists)
gh api repos/OWNER/REPO/rulesets -X POST --input .github/rulesets/develop.json
gh api repos/OWNER/REPO/rulesets -X POST --input .github/rulesets/main.json

See .github/rulesets/ for the complete ruleset definitions.

Automation Bypass for CI/CD

The nightly export workflow commits directly to develop, bypassing branch protection. This is intentional and correct for the ALM pattern.

Why Automation Bypasses Branch Protection

ConcernExplanation
"Shouldn't all changes require PR?"No. PRs are for human-authored changes. Automated exports are operational, not developmental.
"What about review?"QA environment IS the review. Blocking before QA adds delay without adding validation.
"What if bad changes export?"If someone shouldn't change Dev, fix permissions. Don't slow the feedback loop for everyone.

Where Gates Should Be

Dev → develop → QA       (automated, fast feedback)
QA  → main    → Prod     (gated, human approval required)

The human gate belongs at QA → Prod, not at Dev → QA. QA is where you validate changes through testing, not through XML diff review.

Configuration by Repository Type

Organization Repositories (Enterprise):

Add GitHub Actions to the ruleset bypass list:

  1. Repository Settings → Rules → Rulesets → "Develop Branch Rules"
  2. Under "Bypass list", add "GitHub Actions"
  3. Save

Personal Repositories:

Use a Personal Access Token (PAT):

  1. Create fine-grained PAT with contents: write for the repo
  2. Store as AUTOMATION_TOKEN repository secret
  3. Workflow uses PAT for checkout: token: ${{ secrets.AUTOMATION_TOKEN }}

This is the standard pattern when automation needs to bypass branch protection.

What This Enables

  • Nightly exports commit directly to develop
  • QA deployment triggers automatically on push
  • Fast feedback - issues discovered within 24 hours
  • Human PRs (feature branches) still require approval via GITHUB_TOKEN

Commit Message Convention

Follow conventional commits for clear history:

<type>: <short description>

[optional body]

[optional footer]

Types:

TypeDescription
featNew feature or component
fixBug fix
docsDocumentation changes
choreMaintenance, dependencies
refactorCode restructuring

Examples:

feat: add account validation plugin
fix: correct status transition in workflow
docs: update deployment guide
chore: sync solution from Dev environment

When to Deviate

Add Release Branches When:

  • You need to maintain multiple production versions
  • Formal release cycles require stabilization periods
  • Hotfixes need isolation from ongoing development

Add Environment-Specific Branches When:

  • Multiple long-lived environments need different configurations
  • UAT requires extended testing periods
  • Regulatory requirements mandate branch-per-environment

Skip Feature Branches When:

  • Solo developer working on simple changes
  • Automated exports are the only commits
  • Changes are trivial (typos, config adjustments)

How is this guide?

Last updated on

On this page