- Published on
GitOps + Git Workflow Complete Guide — Branch Strategy, PR Rules, and Deployment Pipelines
- Authors
- Name
- Introduction
- 1. GitOps Core Principles
- 2. Branch Strategy Comparison
- 3. PR (Pull Request) Rules
- 4. Commit Conventions
- 5. ArgoCD GitOps Pipeline
- 6. CI/CD Pipeline Design
- 7. Per-Environment Deployment Strategy
- 8. Operational Checklist
- 9. Common Mistakes
- 10. Summary
- Quiz
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.
| Principle | Key Question | Failure Symptom |
|---|---|---|
| Declarative | Is the desired state defined as code? | Configuration drift, non-reproducible |
| Versioned | Are all changes recorded in Git? | Change tracking impossible, unclear accountability |
| Pulled Automatically | Are changes applied without manual intervention? | Deployment delays, human errors |
| Self-Healing | Is 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
| Criteria | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| Branch types | 5+ types | 2 types | 1-2 types |
| Merge frequency | 1-2x/week | 1-3x/day | 5-10x+/day |
| Release cycle | Bi-weekly/monthly | On-demand | On-demand |
| Complexity | High | Low | Low |
| Feature flags required | No | Optional | Required |
| CI requirements | Medium | High | Very high |
| Rollback ease | Release tags | Per-commit | Commit/flag |
| GitOps compatibility | Fair | Good | Excellent |
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:
- Functional correctness — Does the change work as intended?
- Edge cases — Are empty values, large volumes, concurrent requests handled?
- Security — No SQL injection, XSS, or secret exposure risks?
- Performance — No N+1 queries, unnecessary computation, or memory leaks?
- Test coverage — Are there tests for the changed logic?
- Naming conventions — Do variable, function, and class names convey meaning well?
3.3 Merge Strategies
| Strategy | Commit History | Best For |
|---|---|---|
| Squash Merge | Combines all feature commits into one | Keeping main history clean |
| Rebase Merge | Replays feature commits on top of main | Well-structured, logical commits |
| Merge Commit | Creates a merge commit | Preserving 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:
| Type | Description | Example |
|---|---|---|
feat | New feature | feat(auth): add social login |
fix | Bug fix | fix(api): correct pagination offset error |
docs | Documentation change | docs: update API spec |
style | Formatting, semicolons, etc. | style: apply ESLint rules |
refactor | Refactoring | refactor(db): optimize queries |
test | Add/modify tests | test: add signup E2E test |
chore | Build, tool configuration | chore: upgrade to Node.js 20 |
ci | CI configuration change | ci: optimize GitHub Actions cache |
perf | Performance improvement | perf(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
| Option | Description | Recommendation |
|---|---|---|
automated.prune | Auto-remove resources deleted from Git | Production: true |
automated.selfHeal | Auto-correct manual changes | Production: true |
syncOptions.CreateNamespace | Auto-create namespaces | Dev: true, Prod: false |
retry.limit | Retry count on sync failure | 3-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
| Environment | Purpose | Sync Mode | Approval Required |
|---|---|---|---|
| dev | Feature development, integration testing | Auto Sync | No |
| staging | QA, performance testing, UAT | Auto Sync | No |
| production | Live service | Manual Sync | Yes |
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.
| Category | Item | Status |
|---|---|---|
| Branch Protection | Block direct pushes to main | [ ] |
| Branch Protection | Require PR before merging | [ ] |
| Branch Protection | Block force pushes | [ ] |
| Code Review | Require minimum 1 reviewer approval | [ ] |
| Code Review | Configure CODEOWNERS file | [ ] |
| Code Review | Enable stale review dismissal | [ ] |
| CI Gates | Require CI pass on PR (required status checks) | [ ] |
| CI Gates | Set test coverage threshold | [ ] |
| CI Gates | Require lint/format checks to pass | [ ] |
| Commits | Configure commitlint + husky | [ ] |
| Commits | Document Conventional Commits guide | [ ] |
| Security | Enable GitHub Secret Scanning | [ ] |
| Security | Add sensitive file patterns to .gitignore | [ ] |
| Security | Enable Dependabot / Renovate | [ ] |
| ArgoCD | Manage Application resources in Git | [ ] |
| ArgoCD | Set up sync notifications (Slack/Teams) | [ ] |
| ArgoCD | Configure RBAC (project permissions per team) | [ ] |
| ArgoCD | Set 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.
Keeping app code and manifests in the same repository — Every app CI run triggers an ArgoCD sync. Always separate manifests into a dedicated repository.
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.Using the
latesttag in production — Makes it impossible to track which version is deployed. Always use immutable tags (SHA, semantic version).Auto-merging without PR review — Manifest change PRs must also go through review. Allow auto-merge only for dev environments.
Running ArgoCD without RBAC — If every team member can Sync/Delete all applications, incidents will happen. Minimize permissions with Project + RBAC.
Writing meaningless commit messages — Commit messages like
fix,update,testare incomprehensible three months later. Enforce Conventional Commits.Keeping feature branches alive too long — Feature branches alive for more than 3 days become merge conflict breeding grounds. Merge small PRs frequently.
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.
Hardcoding per-environment differences — Instead of creating separate YAML files for dev/staging/prod differences, manage them with Kustomize overlays or Helm values.
Not documenting rollback procedures — Every team member must know the procedure: "git revert, then ArgoCD sync." Create a runbook.
10. Summary
| Area | Key Points |
|---|---|
| GitOps Principles | Declarative, Versioned, Auto-applied, Self-healing — all four must be satisfied for true GitOps |
| Branch Strategy | GitHub Flow or Trunk-Based is optimal for GitOps. Choose based on team size and release cycle |
| PR Rules | Template + Checklist + Min 1 reviewer + CI gates = the last line of defense for quality |
| Commit Conventions | Enforce mechanically with Conventional Commits + commitlint. Do not rely on people |
| ArgoCD | Standardize Application YAML, syncPolicy, and directory structure; manage in Git |
| CI/CD | Automate the flow: App Repo -> CI (build/test/push) -> Manifest Repo PR -> ArgoCD Sync |
| Environment Separation | Manage per-environment differences with Kustomize overlays or Helm values-env.yaml |
| Operations | Include 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.