Merge Strategies in GitOps Repos - Squash, Rebase, and Main-Branch Discipline

Status: Active
Last Updated: 2026-08-26
Category: GitOps - Workflow
Prerequisites: git-fundamentals, gitops
Time: 2 hours
Tags: git, merge, squash, rebase, gitops, release, atomic-commits

Summary

How to choose between merge, squash, and rebase commits in a GitOps repository, why every manifest change must be a single atomic commit, and how to tag releases so reconcilers and rollback procedures always have an unambiguous target.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Context / Why This Matters
  2. Merge vs Squash vs Rebase
  3. Main-Branch Discipline
  4. Atomic Manifest Commits
  5. Release Tagging

Context / Why This Matters

In a GitOps repo the commit history is the deployment history. A normal application repo's messy history is cosmetic; a manifests repo's messy history makes git revert dangerous, rollbacks ambiguous, and audits incomplete. gitops ties every change to a ticket, a branch, and a Forgejo Action โ€” merge strategy is what keeps that chain clean end-to-end.

This article applies general Git practice from git-fundamentals to the specific constraints of declarative infrastructure, complementing cicd-concepts.


Merge vs Squash vs Rebase

Strategy History contains Best for Avoid for
Merge commit (--no-ff) All WIP commits + merge node Long-lived integration branches, hotfixes needing full forensic trail Manifest repos (pollutes deployment history)
Squash (--squash) Exactly one commit per MR Feature work, docs changes โ€” default choice Multi-part changes that should revert independently
Rebase (rebase + ff) Linear history, each logical step preserved Carefully crafted PRs with meaningful steps Shared/public branches (rewrites others' history)

Recommendation for fogserv.cloud

Manifests / config repos : SQUASH. One MR = one commit = one deployment event.
App source repos         : developer's choice; squash preferred.
Hotfixes                 : SQUASH with ticket ID in the subject line.
Dependency bumps         : SQUASH, batched weekly into one MR per ecosystem.

Rationale: Flux/Argo sync on commit SHA. With squash, "which commit changed this resource?" has exactly one answer, and reverting a deployment is one git revert <sha> instead of untangling interleaved WIP commits.

Enforcing it in Forgejo

Set protected-branch settings on main:


Main-Branch Discipline

main in a GitOps repo is not "work in progress" โ€” it is what production will look like after the next reconcile. Rules:

  1. Every commit on main must be applyable. CI renders manifests (kustomize build, helm template) and fails the check if rendering breaks. Never merge "to test".
  2. No direct pushes. Everything arrives via MR, even one-line fixes โ€” that is what creates the audit trail gitops requires.
  3. Staging before prod. Same commit promoted by directory overlay (clusters/staging โ†’ clusters/prod), never divergent edits.
  4. Revert forward-looking: to undo a bad change, revert the commit; do not push a "counter-change" that adds confusion.
  5. Short-lived branches. Merge within days; stale branches drift against a moving main and produce surprise conflicts exactly when you can least afford them.

Branch naming tied to tickets

git checkout -b ticket-142/api-replica-autoscaling   # Forgejo Issue #142

The ticket number in the branch name (and squashed commit subject) links deployment telemetry back to the issue automatically, as required by the gitops workflow.


Atomic Manifest Commits

One logical change = one MR = one squashed commit. An atomic commit:

Example: good vs bad diff scope

# GOOD - atomic: one purpose, self-consistent
 clusters/prod/apps/api/deployment.yaml
-image: harbor.fogserv.cloud/prod/api:2.3.1
+image: harbor.fogserv.cloud/prod/api:2.3.2

 clusters/prod/apps/api/kustomization.yaml
-images:
-  - newTag: 2.3.1
+images:
+  - newTag: 2.3.2

# BAD - two unrelated concerns entangled
 clusters/prod/apps/api/deployment.yaml     (image bump)
 clusters/prod/apps/web/ingress.yaml        (unrelated TLS change)

If both were merged atomically-bad and the TLS change broke traffic, reverting would also roll back the API image โ€” or vice versa. Split them.

Commit message convention

<type>(<scope>): <summary> (#ticket)

api(deploy): bump api to 2.3.2 for CVE-2026-1234 (#142)

Rendered with kustomize 5.4.0; staging soak passed 24h.

Types: deploy, infra, policy, docs, chore. The KB update demanded by documentation-as-code rides along in the same MR whenever behavior changes.


Release Tagging

Tags give reconcilers and humans a stable name for "the state we certified".

# After staging soak passes
git tag -a prod-v2.3.2 -m "Prod promotion of api 2.3.2, ticket #142"
git push origin prod-v2.3.2

Usage patterns:

Flux note: pin the source ref.name to a branch but record the resolved SHA per environment; Argo CD users can pin targetRevision to a tag directly per app.


Practical Examples

Example 1: Clean hotfix lifecycle end-to-end

# 1. Branch from main
git checkout -b hotfix/ticket-201-ingress-tls main

# 2. Minimal fix, single concern
$EDITOR clusters/prod/ingress/web.yaml
git add clusters/prod/ingress/web.yaml
git commit -m "ingress(web): restore TLS secret name after rotation (#201)"

# 3. Push, MR, squash-merge once checks pass
git push origin hotfix/ticket-201-ingress-tls

# 4. If it was wrong: surgical undo
git revert <squash-sha>
git push   # MR auto-created; reconciler converges after merge

Example 2: Batched dependency bumps

One weekly MR titled chore(deps): weekly bumps 2026-W35, containing all Renovate-style updates across overlays. Squashed to one commit; if any bump misbehaves, bisect within the MR branch before merge, not after โ€” post-merge, revert the whole batch and re-split rather than debugging in prod.


Troubleshooting & Common Pitfalls

Problem Cause Fix
Revert of a merge commit does nothing Default revert assumes non-merge parent Use git revert -m 1 <sha>, or prefer squash merges so this never arises
Reconciler stuck on old revision Force-push rewrote main history Never force-push main; recover by pushing a corrective commit
Two MRs conflict over generated lockfile Parallel automation writing same file Serialize automation jobs or use a dedicated bot branch with rebase
"It worked in staging" but prod differs Divergent hand-edits instead of overlay promotion Diff overlays: diff -r clusters/staging clusters/prod; fix by refactoring to parameterized base
Deployment history untraceable Merge commits with WIP noise Switch repo policy to squash-only on protected main

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse