Skip to content
Published on

GitOps + Git Workflow Complete Guide — Branch Strategy, PR Rules, and Deployment Pipelines

Authors
  • Name
    Twitter

Introduction

"Code is infrastructure." GitOps turns this single statement into operational reality. By treating a Git repository as the Single Source of Truth (SSOT), every change committed to declarative configuration files directly represents the state of your infrastructure and applications. No more running kubectl apply manually or asking someone in Slack to "please deploy."

But adopting GitOps does not automatically organize everything. If your branch strategy is a mess, your deployment pipeline will be a mess too. Without PR rules, code quality cannot be guaranteed. With inconsistent commit messages, change tracking becomes impossible. The true power of GitOps emerges only when the entire Git workflow is systematically designed.

This guide covers everything from GitOps core principles, branch strategy comparison, PR rule design, commit conventions, ArgoCD pipeline configuration, per-environment deployment strategies, and operational checklists — all at a level your team can immediately apply.


1. GitOps Core Principles

GitOps was proposed by Weaveworks in 2017 and is built on the following four principles.

1.1 Declarative

All system state is defined declaratively. Instead of "run this command," you describe "this is the desired state."

# Declarative state definition — Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api-server
          image: ghcr.io/myorg/api-server:v2.4.1
          ports:
            - containerPort: 8080

1.2 Versioned and Immutable

All changes are recorded as Git commits. Who, when, why, and what changed — a perfect audit log is automatically maintained. Rollback is just a git revert away.

1.3 Pulled Automatically

Approved changes are automatically applied to the system. Instead of the push model (CI deploys directly), GitOps uses the pull model (an agent watches Git, detects changes, and applies them).

1.4 Continuously Reconciled (Self-Healing)

If the actual system state differs from the state defined in Git, it is automatically corrected. Even if someone changes something via kubectl, the GitOps agent reverts it to the defined state.

PrincipleKey QuestionFailure Symptom
DeclarativeIs the desired state defined as code?Configuration drift, non-reproducible
VersionedAre all changes recorded in Git?Change tracking impossible, unclear accountability
Pulled AutomaticallyAre changes applied without manual intervention?Deployment delays, human errors
Self-HealingIs drift automatically corrected?Environment inconsistency, cascading failures

2. Branch Strategy Comparison

Your branch strategy should be chosen based on team size, release cycle, and product characteristics. Here we compare three major strategies.

2.1 Git Flow

The classic model proposed by Vincent Driessen in 2010. It uses five branch types: main, develop, feature/*, release/*, and hotfix/*.

main ────●─────────────●──────●── (production)
          \           / \    /
release    \    ●────●   \  /
            \  /          \/
develop ─────●──●──●──●───●──●── (integration)
              \  \      /
feature        ●──●────●

Best for: Teams with regular release cycles, mobile apps, package libraries

2.2 GitHub Flow

A simple model using only main and feature branches. Work on a feature branch and merge to main via PR.

main ────●──●──●──●──●── (always deployable)
          \    /  \  /
feature    ●──●    ●──●

Best for: SaaS teams practicing continuous deployment, small teams

2.3 Trunk-Based Development (TBD)

All developers commit directly to trunk (main) or use very short-lived branches (less than 1 day).

main ────●──●──●──●──●──●──●── (every commit is a deploy candidate)
          \/ \/ \/
short     ●  ●    (merged within 1 day)

Best for: Large engineering organizations (Google, Meta), feature-flag-based releases

Comparison Table

CriteriaGit FlowGitHub FlowTrunk-Based
Branch types5+ types2 types1-2 types
Merge frequency1-2x/week1-3x/day5-10x+/day
Release cycleBi-weekly/monthlyOn-demandOn-demand
ComplexityHighLowLow
Feature flags requiredNoOptionalRequired
CI requirementsMediumHighVery high
Rollback easeRelease tagsPer-commitCommit/flag
GitOps compatibilityFairGoodExcellent

Recommendation: For GitOps environments, GitHub Flow or Trunk-Based Development is recommended. Git Flow's release branches create friction with GitOps auto-sync patterns.

3. PR (Pull Request) Rules

PRs are not just code review tools. They are the core process for documenting change intent, enforcing quality gates, and sharing team knowledge.

3.1 PR Template

Add a .github/PULL_REQUEST_TEMPLATE.md file to the repository root.

## Changes

<!-- Briefly describe what was changed and why -->

## Change Type

- [ ] Feature (feature)
- [ ] Bug fix (bugfix)
- [ ] Refactoring (refactor)
- [ ] Infrastructure/config change (infra)
- [ ] Documentation update (docs)

## Testing

- [ ] Unit tests added/updated
- [ ] Integration tests passing
- [ ] Manually verified in local environment

## Checklist

- [ ] Commit messages follow Conventional Commits format?
- [ ] No unnecessary files (logs, temp files) included?
- [ ] Backward compatible with existing APIs?
- [ ] No security-sensitive information (secrets, keys) included?

## Related Issues

Closes #

## Screenshots (for UI changes)

3.2 Review Checklist

Key items reviewers should verify:

  1. Functional correctness — Does the change work as intended?
  2. Edge cases — Are empty values, large volumes, concurrent requests handled?
  3. Security — No SQL injection, XSS, or secret exposure risks?
  4. Performance — No N+1 queries, unnecessary computation, or memory leaks?
  5. Test coverage — Are there tests for the changed logic?
  6. Naming conventions — Do variable, function, and class names convey meaning well?

3.3 Merge Strategies

StrategyCommit HistoryBest For
Squash MergeCombines all feature commits into oneKeeping main history clean
Rebase MergeReplays feature commits on top of mainWell-structured, logical commits
Merge CommitCreates a merge commitPreserving feature branch context

Team recommendation: Set Squash Merge as the default and prohibit direct pushes to main. Enable "Require a pull request before merging" in GitHub Settings > Branches > Branch protection rules.

4. Commit Conventions

4.1 Conventional Commits Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Type list:

TypeDescriptionExample
featNew featurefeat(auth): add social login
fixBug fixfix(api): correct pagination offset error
docsDocumentation changedocs: update API spec
styleFormatting, semicolons, etc.style: apply ESLint rules
refactorRefactoringrefactor(db): optimize queries
testAdd/modify teststest: add signup E2E test
choreBuild, tool configurationchore: upgrade to Node.js 20
ciCI configuration changeci: optimize GitHub Actions cache
perfPerformance improvementperf(api): apply response caching

4.2 commitlint Setup

# Install
npm install --save-dev @commitlint/cli @commitlint/config-conventional

# commitlint.config.js
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

# Connect with husky Git hook
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

You can customize team rules in commitlint.config.js.

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'ci', 'perf', 'revert'],
    ],
    'subject-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 100],
  },
}

5. ArgoCD GitOps Pipeline

5.1 ArgoCD Architecture

ArgoCD is a Kubernetes-native GitOps deployment tool composed of the following components:

  • API Server — Provides Web UI, CLI, gRPC/REST API
  • Repository Server — Renders manifests from Git repositories
  • Application Controller — Continuously compares actual state with desired state and synchronizes

5.2 Application Definition

# argocd/applications/api-server.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-server
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/k8s-manifests.git
    targetRevision: main
    path: apps/api-server/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true # Auto-remove resources deleted from Git
      selfHeal: true # Auto-correct drift
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

5.3 Sync Policy Options

OptionDescriptionRecommendation
automated.pruneAuto-remove resources deleted from GitProduction: true
automated.selfHealAuto-correct manual changesProduction: true
syncOptions.CreateNamespaceAuto-create namespacesDev: true, Prod: false
retry.limitRetry count on sync failure3-5

5.4 Git Repository Directory Structure

k8s-manifests/
├── apps/
│   ├── api-server/
│   │   ├── base/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   ├── hpa.yaml
│   │   │   └── kustomization.yaml
│   │   └── overlays/
│   │       ├── dev/
│   │       │   ├── kustomization.yaml
│   │       │   └── patches/
│   │       │       └── replicas.yaml
│   │       ├── staging/
│   │       │   ├── kustomization.yaml
│   │       │   └── patches/
│   │       │       └── replicas.yaml
│   │       └── production/
│   │           ├── kustomization.yaml
│   │           └── patches/
│   │               ├── replicas.yaml
│   │               └── resources.yaml
│   └── web-frontend/
│       ├── base/
│       └── overlays/
├── argocd/
│   ├── applications/
│   │   ├── api-server.yaml
│   │   └── web-frontend.yaml
│   └── projects/
│       └── default.yaml
└── README.md

6. CI/CD Pipeline Design

6.1 Overall Flow

graph LR
    A[Developer Push] --> B[GitHub Actions CI]
    B --> C{Tests Pass?}
    C -->|Yes| D[Build Container Image]
    C -->|No| E[Failure Notification]
    D --> F[Push Image to GHCR]
    F --> G[Create PR in Manifest Repo]
    G --> H[Auto-merge / Manual Approval]
    H --> I[ArgoCD Detects Change]
    I --> J[Kubernetes Sync]
    J --> K{Health Check}
    K -->|Healthy| L[Deployment Complete]
    K -->|Degraded| M[Auto Rollback]

6.2 GitHub Actions Workflow

# .github/workflows/ci-cd.yaml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Unit tests
        run: npm run test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build-and-push:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Checkout manifest repo
        uses: actions/checkout@v4
        with:
          repository: myorg/k8s-manifests
          token: ${{ secrets.MANIFEST_REPO_TOKEN }}

      - name: Update image tag
        run: |
          cd apps/api-server/base
          kustomize edit set image \
            ghcr.io/myorg/api-server=ghcr.io/myorg/api-server:${{ needs.build-and-push.outputs.image-tag }}

      - name: Create PR
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.MANIFEST_REPO_TOKEN }}
          commit-message: 'chore: update api-server image to ${{ needs.build-and-push.outputs.image-tag }}'
          title: 'deploy: api-server ${{ needs.build-and-push.outputs.image-tag }}'
          body: |
            Auto-generated deployment PR.
            - Source commit: ${{ github.sha }}
            - Image tag: ${{ needs.build-and-push.outputs.image-tag }}
          branch: deploy/api-server-${{ needs.build-and-push.outputs.image-tag }}
          base: main

7. Per-Environment Deployment Strategy

7.1 Environment Separation Principles

EnvironmentPurposeSync ModeApproval Required
devFeature development, integration testingAuto SyncNo
stagingQA, performance testing, UATAuto SyncNo
productionLive serviceManual SyncYes

7.2 Kustomize Overlay Configuration

base/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - hpa.yaml
commonLabels:
  app: api-server

overlays/dev/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namePrefix: dev-
namespace: dev
patches:
  - path: patches/replicas.yaml
images:
  - name: ghcr.io/myorg/api-server
    newTag: latest

overlays/dev/patches/replicas.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: api-server
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 200m
              memory: 256Mi

overlays/production/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: production
patches:
  - path: patches/replicas.yaml
  - path: patches/resources.yaml
images:
  - name: ghcr.io/myorg/api-server
    newTag: v2.4.1 # Production uses explicit tags

overlays/production/patches/replicas.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 5
  template:
    spec:
      containers:
        - name: api-server
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: '1'
              memory: 1Gi

7.3 Helm Values per Environment

When using Helm, separate values files per environment.

helm-charts/
├── api-server/
│   ├── Chart.yaml
│   ├── templates/
│   ├── values.yaml            # defaults
│   ├── values-dev.yaml        # dev environment
│   ├── values-staging.yaml    # staging environment
│   └── values-prod.yaml       # production environment
# values-prod.yaml
replicaCount: 5
image:
  repository: ghcr.io/myorg/api-server
  tag: v2.4.1
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: '1'
    memory: 1Gi
autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
  targetCPUUtilization: 70
ingress:
  enabled: true
  hosts:
    - host: api.myservice.com
      paths:
        - path: /
          pathType: Prefix

8. Operational Checklist

Essential items to verify when operating a GitOps workflow as a team.

CategoryItemStatus
Branch ProtectionBlock direct pushes to main[ ]
Branch ProtectionRequire PR before merging[ ]
Branch ProtectionBlock force pushes[ ]
Code ReviewRequire minimum 1 reviewer approval[ ]
Code ReviewConfigure CODEOWNERS file[ ]
Code ReviewEnable stale review dismissal[ ]
CI GatesRequire CI pass on PR (required status checks)[ ]
CI GatesSet test coverage threshold[ ]
CI GatesRequire lint/format checks to pass[ ]
CommitsConfigure commitlint + husky[ ]
CommitsDocument Conventional Commits guide[ ]
SecurityEnable GitHub Secret Scanning[ ]
SecurityAdd sensitive file patterns to .gitignore[ ]
SecurityEnable Dependabot / Renovate[ ]
ArgoCDManage Application resources in Git[ ]
ArgoCDSet up sync notifications (Slack/Teams)[ ]
ArgoCDConfigure RBAC (project permissions per team)[ ]
ArgoCDSet production to manual sync[ ]

CODEOWNERS file example:

# .github/CODEOWNERS
# Default owners
*                       @myorg/platform-team

# Infrastructure manifests
/apps/                  @myorg/sre-team
/argocd/                @myorg/sre-team

# Frontend
/apps/web-frontend/     @myorg/frontend-team

# Backend API
/apps/api-server/       @myorg/backend-team

9. Common Mistakes

Here are the mistakes teams frequently make when adopting GitOps and Git workflows.

  1. Keeping app code and manifests in the same repository — Every app CI run triggers an ArgoCD sync. Always separate manifests into a dedicated repository.

  2. Enabling Auto Sync + Auto Prune in production — An accidentally merged deletion is immediately applied to production. Use Manual Sync for production, or at minimum only enable selfHeal.

  3. Using the latest tag in production — Makes it impossible to track which version is deployed. Always use immutable tags (SHA, semantic version).

  4. Auto-merging without PR review — Manifest change PRs must also go through review. Allow auto-merge only for dev environments.

  5. Running ArgoCD without RBAC — If every team member can Sync/Delete all applications, incidents will happen. Minimize permissions with Project + RBAC.

  6. Writing meaningless commit messages — Commit messages like fix, update, test are incomprehensible three months later. Enforce Conventional Commits.

  7. Keeping feature branches alive too long — Feature branches alive for more than 3 days become merge conflict breeding grounds. Merge small PRs frequently.

  8. Committing secrets to Git — Any secret that has ever been pushed to Git remains in history forever. Use Sealed Secrets, External Secrets Operator, or Vault.

  9. Hardcoding per-environment differences — Instead of creating separate YAML files for dev/staging/prod differences, manage them with Kustomize overlays or Helm values.

  10. Not documenting rollback procedures — Every team member must know the procedure: "git revert, then ArgoCD sync." Create a runbook.

10. Summary

AreaKey Points
GitOps PrinciplesDeclarative, Versioned, Auto-applied, Self-healing — all four must be satisfied for true GitOps
Branch StrategyGitHub Flow or Trunk-Based is optimal for GitOps. Choose based on team size and release cycle
PR RulesTemplate + Checklist + Min 1 reviewer + CI gates = the last line of defense for quality
Commit ConventionsEnforce mechanically with Conventional Commits + commitlint. Do not rely on people
ArgoCDStandardize Application YAML, syncPolicy, and directory structure; manage in Git
CI/CDAutomate the flow: App Repo -> CI (build/test/push) -> Manifest Repo PR -> ArgoCD Sync
Environment SeparationManage per-environment differences with Kustomize overlays or Helm values-env.yaml
OperationsInclude checklists in team onboarding docs and review regularly

GitOps is not a tool — it is a culture. Installing ArgoCD is not the end. The key is for the entire team to internalize Git-centric workflows and follow the rules. Apply the practices from this guide one by one, and build a workflow tailored to your team.

Quiz

Q1: What are the four core principles of GitOps? The four principles are Declarative, Versioned and Immutable, Pulled Automatically, and Continuously Reconciled (Self-Healing). All four must be satisfied for a true GitOps implementation. Missing even one reduces it to merely "Git-based deployment."

Q2: Among Git Flow, GitHub Flow, and Trunk-Based Development, which branch strategy best fits a GitOps environment?

GitHub Flow or Trunk-Based Development are the best fits for GitOps environments. Git Flow's release branches create friction with GitOps auto-sync patterns, and a simpler branch structure aligns better with declarative state management. Trunk-Based Development, combined with feature flags, enables the fastest deployment cycles.

Q3: What are the differences between Squash Merge, Rebase Merge, and Merge Commit, and when is each appropriate?

Squash Merge combines all feature branch commits into one, ideal for keeping a clean main history. Rebase Merge replays individual commits on top of main, best when commits are logically well-structured. Merge Commit creates a merge commit preserving the feature branch context (when it started and was merged). Most teams recommend Squash Merge as the default.

Q4: What is ArgoCD's selfHeal option and why is it important? selfHeal automatically corrects the actual Kubernetes cluster state when it differs from the state defined in Git. For example, if someone directly changes a Deployment's replicas via kubectl, ArgoCD detects this and reverts it to the Git-defined value. This implements GitOps' "self-healing" principle and is essential for preventing configuration drift and maintaining Git as the Single Source of Truth (SSOT).

Q5: Why should the app code repository and manifest repository be separated? When app code and manifests are in the same repository, every app CI run triggers unnecessary ArgoCD syncs. It also makes it difficult to separate access permissions between app developers and infrastructure operators, and harder to maintain an independent review process for manifest changes. Separating repositories provides clearer separation of concerns, permission management, and deployment tracking.

Q6: In Conventional Commits, what do feat, fix, and chore mean? feat is used when adding a new user-facing feature, fix when fixing a bug in existing functionality, and chore for build system or tooling changes that do not directly affect user features. These rules can be automatically validated with commitlint, and when integrated with semantic-release, versions can be automatically bumped based on commit types.

Q7: How do you manage per-environment deployments with Kustomize overlays? Place common manifests in the base directory, and define per-environment differences as patches in overlays/{dev,staging,production} directories. For example, base contains Deployment, Service, HPA, etc., the dev overlay patches replicas to 1, and the production overlay patches replicas to 5. Setting the ArgoCD Application's source.path to the appropriate overlay directory completes the per-environment deployment setup.

Q8: How do you safely manage secrets in a GitOps environment? Secrets must never be committed to Git in plaintext. Alternatives include Sealed Secrets (Bitnami), which encrypts with a public key for Git storage and decrypts only within the cluster. External Secrets Operator integrates with external secret management services like AWS Secrets Manager or HashiCorp Vault to inject secrets at runtime. SOPS (Mozilla) encrypts YAML file values with KMS keys for Git storage. Choose the appropriate tool based on your team size and infrastructure.