Git Best Practices
Git conventions for repositories that hold Kubernetes manifests, Helm charts, and Dockerfiles, where a merge is a production change.
How to Use This List
These practices assume a manifest repo reconciled by Argo CD or Flux, so the default branch is the desired state of a cluster.
Most of this is portable Git hygiene, but the emphasis shifts: a bad merge here does not fail a build, it rolls out.
Adopt group A first - branch protection is what makes every later practice enforceable rather than advisory.
A - Protect the Default Branch
Require pull requests for every change to the default branch. In GitHub repository rulesets (Settings, Rules, Rulesets) enable "Require a pull request before merging" and set the target to the default branch. Direct pushes to a reconciled branch bypass every check you own.
Require status checks to pass, and mark them strictly required. Enable "Require status checks to pass" and add your kubeconform, conftest, and helm template jobs by name. Enable "Require branches to be up to date before merging" so a check cannot pass against a stale base.
Block force pushes and deletions on the default branch. Rulesets have explicit "Restrict force pushes" and "Restrict deletions" toggles. Argo CD reconciles a ref, so a rewritten history is a silent, unreviewed cluster change.
Require at least one approving review, and dismiss stale approvals. Turn on "Dismiss stale pull request approvals when new commits are pushed" so an approval never carries over to code nobody read.
Apply protection to release branches with a pattern. Rulesets accept fnmatch targets such as refs/heads/release/*, so release/1.36 gets the same guarantees as main without hand-configuring each one.
Do not grant bypass to humans by default. The bypass list in a ruleset should hold only the app or bot that needs it, such as an image updater, and even that is worth questioning.
B - Own the Right Paths with CODEOWNERS
Put CODEOWNERS in .github/ and make ownership match blast radius. The last matching pattern wins, so order the file from general to specific.
# Default owner for anything not matched below
* @acme/platform
# App teams own their own overlays
/apps/checkout/ @acme/checkout
/apps/search/ @acme/search
# Cluster-wide risk stays with platform, even inside app dirs
/apps/*/networkpolicy.yaml @acme/platform
/clusters/ @acme/platform @acme/security
/.github/workflows/ @acme/platform
Enable "Require review from Code Owners" in the ruleset. Without that toggle, CODEOWNERS only requests reviews - it does not require them.
Guard the guards. Include /.github/ and your policy directory in CODEOWNERS so a PR cannot quietly weaken the checks that gate every other PR.
Use teams, never individuals, as owners. An individual owner becomes a merge blocker the week they take leave.
Verify that owning teams have write access. A team listed in CODEOWNERS without write permission on the repo is skipped for review requests, which fails open and is easy to miss.
C - Commit and Branch Hygiene
Write commits that explain the operational intent, not the diff. "Raise checkout memory limit to 1Gi after OOMKills in prod" is reviewable; "update yaml" forces the reviewer to reverse-engineer the change.
Keep one logical change per commit, and one concern per PR. Mixing a base image bump with an HPA retune means a revert cannot separate them.
Prefer squash merges for app changes and keep the PR title as the subject. A linear history of one commit per merged PR makes git revert <sha> a trustworthy rollback of a whole change.
Use short-lived branches off the default branch. Long-lived feature branches on manifests drift from the live cluster state and produce merge conflicts in exactly the fields that matter, like image tags and replica counts.
Enable "Require linear history" if you rely on revert-by-SHA. It blocks merge commits, which keeps the reconciled ref easy to reason about.
Adopt signed commits where you can enforce them. Rulesets have a "Require signed commits" option; pair it with git config --global commit.gpgsign true and SSH or GPG signing keys so authorship is attestable.
D - Repository Structure and Secrets
Separate application source from deployment manifests. A change to the manifest repo triggers a rollout; a change to the app repo triggers a build. Collapsing them means every code commit is a candidate cluster change.
Never commit plaintext Secrets, even to a private repo. Git history is permanent and widely cloned. Use Sealed Secrets, SOPS with age or KMS, or an External Secrets Operator that pulls from a real secret store at runtime.
Add a secret scanner and treat any hit as a rotation event. GitHub push protection blocks known credential formats at push time; a .gitignore does not, and cleaning history does not un-leak a key.
Pin base images and workload images by digest, not floating tags. image: registry/app@sha256:... makes the Git SHA and the running bits a single, auditable fact; :latest makes the repo a lie.
Keep .gitignore honest about generated output. Rendered manifests, charts/ vendored dependencies, and local kubeconfigs do not belong in the repo unless rendering is deliberately your workflow.
Pin GitHub Actions to a commit SHA. uses: actions/checkout@<sha> is the only form that cannot be moved under you; tags are mutable and CI has repo credentials.
E - Reviewing Manifest Changes
Review the rendered output, not just the source. Have CI post helm template or kustomize build diffs, or use the Argo CD PR bot, so a reviewer sees what actually lands in the cluster.
Treat these fields as high-risk and review them explicitly: image, replicas, resources, securityContext, RBAC rules, NetworkPolicy selectors, and anything touching an Ingress or Gateway. Everything else is usually reversible.
Make CI check what humans reliably miss. Schema validity via kubeconform, policy via conftest or kyverno apply, and image provenance are all mechanical - do not spend review attention on them.
Require the PR to say how it rolls back. For a manifest change, the answer is usually "revert the commit and let Argo CD reconcile," and it is worth confirming that is actually true.
Reject PRs that disable a check to make themselves pass. A diff that edits both a policy and the manifest that violates it is a red flag worth a second reviewer.
When You Are Done
Your default branch should reject direct pushes, force pushes, and deletions from everyone, with no human bypass entries.
Every path in the repo should have an owning team, and the riskiest paths - /clusters/, /.github/, policy - should be owned by platform or security regardless of who else touches them.
Every merged commit should be revertable on its own, and no workload image should be referenced by a mutable tag.
If all of that holds, the Git history is a faithful, auditable record of the cluster - which is the whole point of putting manifests in Git.
FAQs
Are rulesets or classic branch protection the right choice? Rulesets are the current mechanism and are what to build on; they layer, target ref patterns, and support an explicit bypass list. Classic branch protection still works, but rules from both evaluate together, which gets confusing fast.
Should the manifest repo and the app repo be the same repo? Usually not. Separate repos let build and deploy have different owners, different review requirements, and different rates of change.
Does CODEOWNERS review requirement apply to the bot that bumps image tags? Yes, unless the bot is on the ruleset bypass list. Many teams do bypass it for digest-only bumps and rely on the policy checks instead.
Is squash merge always right? It is a good default for app changes. For a manifest repo where individual commits are meaningful rollback units, a rebase merge that preserves them can be better - just keep the history linear either way.
How do I fix a secret that is already in history? Rotate it first, immediately. Rewriting history with git filter-repo is worth doing afterwards, but clones and forks may already have it, so rotation is the only real remediation.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).