18 Best Practices for Version Control in DevOps
18 version control best practices for DevOps: version IaC and pipelines, integrate daily, tag artifacts by SHA, GitOps, OIDC and DORA lead time.
In DevOps, version control is the system of record for delivery. Store everything needed to rebuild a system in Git (application code, infrastructure, pipelines, configuration and migrations), integrate to trunk daily through reviewed pull requests, tie every build and deployment to a commit, use GitOps for runtime state, and keep secrets out.
This guide looks at Git from the delivery pipeline's point of view: what has to be versioned, how changes flow to production, and how the history doubles as an audit trail. It belongs to our Git repository management hub, which covers repository setup, access and maintenance. Two companion guides go deeper on topics only touched on here: Git commit messages and Git branching strategies. Platform features, DORA definitions and the OpenGitOps principles were checked against their official sources in November 2026.
Key takeaways
- If a server, cluster or pipeline cannot be rebuilt from what is in Git (plus a secrets manager), something is missing from version control.
- Pipeline definitions and infrastructure code need the same review and protection as application code.
- Build an artifact once, label it with the commit SHA, and promote that same artifact through every environment.
- DORA's change lead time is measured from commit to production, so your Git history is also your delivery data.
- GitOps makes Git the desired state for clusters, with an agent that pulls and reconciles changes continuously.
- Never store secrets or Terraform state in Git. Use short-lived credentials such as OIDC in CI.
The 18 practices by stage
| Stage | Practices | Question it answers |
|---|---|---|
| What to version | 1 to 5 | Can we rebuild everything from Git? |
| How changes flow | 6 to 9 | How does a change get to main safely? |
| Traceability | 10 to 13 | Which commit is running in production, and how do we undo it? |
| GitOps | 14 | How does Git drive runtime state? |
| Security and audit | 15 to 18 | Who changed what, with which credentials, and can we prove it? |
What to put under version control
1. Version everything needed to rebuild the system
A useful test: if your production environment disappeared, could you recreate it from repositories plus a secrets manager? DevOps teams version far more than application source:
| Version in Git | Keep out of Git |
|---|---|
| Application code and tests | Passwords, API keys, private keys, tokens |
| Infrastructure as code (Terraform, Ansible, CloudFormation) | Terraform state files |
Pipeline definitions (.gitlab-ci.yml, .github/workflows/, Jenkinsfile) | Build output, compiled binaries, container images |
| Kubernetes manifests, Helm charts, Kustomize overlays | Downloaded dependencies (node_modules/, .terraform/) |
| Database schema migrations | Production data and backups |
| Policies (for example OPA or Sentinel), runbooks, architecture decisions | Local IDE and machine settings |
The left column is code and declarations that humans review. The right column is either sensitive or generated, and belongs in a secrets manager, an artifact repository or a state backend.
2. Treat pipeline definitions as production code
A pipeline file can deploy to production and read every secret the CI system holds, so a change to it deserves at least as much review as a change to the application. Keep pipelines in the same repository as the code they build, protect the paths with CODEOWNERS (for example /.github/workflows/ @acme/devops-team), and share common steps through versioned templates or reusable workflows instead of copy-paste. When you reference a shared template or third-party action, pin it to a tag or commit SHA so an upstream change does not alter your pipeline silently.
3. Keep infrastructure code in Git and its state out of it
Infrastructure as code only works if every change goes through the repository. Run terraform plan in the merge request so reviewers see the effect before approving, and apply only from the pipeline after merge. Terraform state is a different matter: HashiCorp's documentation warns that state can contain sensitive values, so store it in a remote backend with locking and encryption, and add *.tfstate and .terraform/ to .gitignore. Our post on applying version control to infrastructure with Terraform walks through a full workflow.
4. Express environment differences as code
Differences between development, staging and production (replica counts, URLs, resource limits) should live in versioned files such as Helm values files or Kustomize overlays, one per environment, in one branch. That way a reviewer can diff environments, and promotion is a change to a version number rather than a merge between environment branches. The branching guide explains why environment branches cause drift.
deploy/
base/ # shared Kubernetes manifests
overlays/
dev/kustomization.yaml
staging/kustomization.yaml
prod/kustomization.yaml
5. Version database changes as migrations
Schema changes made by hand on a production database are invisible to version control and impossible to reproduce. Write each change as an ordered migration file committed with the application code that needs it, run migrations from the pipeline, and design them to be backward compatible (add a column, deploy code that uses it, remove the old column later) so you can roll the application back without rolling the schema back.
How changes flow to main
6. Integrate to trunk at least once a day
Continuous integration means what it says: changes from every developer are combined frequently. DORA's trunk-based development capability describes teams that keep three or fewer active branches and merge to trunk at least once a day, with no code freezes or integration phases. Long-lived branches delay feedback and turn merges into projects. Choosing and enforcing the branch model is covered in detail in the branching strategies guide.
7. Send every change through a reviewed pull request with automated checks
The pull or merge request is where version control meets the pipeline. Before anything merges, CI should build, run tests, lint, scan for secrets and vulnerabilities, and, for infrastructure, show a plan. Protection rules make those checks mandatory: required status checks and approvals on GitHub, and on GitLab protected branches combined with the "Pipelines must succeed" merge check and approval rules. The result is that main only ever contains changes that passed the same gates.
8. Make commits small and traceable
Small commits that each do one thing make reviews quicker and let git bisect and git revert work cleanly when something breaks in production. Link each change to its work item with a trailer such as Closes #482 or Refs: PROJ-1182, so you can follow a production incident back to the ticket that requested the change. The commit message guide covers the format, Conventional Commits and how to lint messages in CI.
9. Separate deployment from release with feature flags
Merging to main daily does not mean users see half-built features. Feature flags let unfinished code ship to production switched off, then be enabled for a percentage of users or a single tenant. That keeps branches short and turns a risky release into a configuration change that can be reversed in seconds. Track flags like code: each needs an owner and a removal date.
Traceability from commit to production
10. Build once and tag artifacts with the commit SHA
Every artifact should say exactly which commit produced it. Both major CI systems expose the commit ID to jobs: GITHUB_SHA in GitHub Actions and CI_COMMIT_SHA in GitLab CI/CD. Use it in image tags and labels, then promote that image unchanged through staging and production. Rebuilding per environment means the thing you tested is not the thing you shipped.
# .gitlab-ci.yml (excerpt)
build-image:
stage: build
image: docker:27
services: [docker:27-dind]
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
11. Version releases with SemVer and annotated tags
Use Semantic Versioning (MAJOR.MINOR.PATCH) for anything other teams or customers consume, and mark each release with an annotated Git tag such as v2.4.0. Tags give humans a readable name for a commit, trigger release pipelines and anchor changelogs. If commits follow Conventional Commits, release tooling can calculate the next version and write the changelog from history. Protect tag patterns so a released version cannot be moved.
12. Record every deployment against a commit
Knowing that version 2.4.0 exists is not enough; you need to know where it is running. GitHub Actions jobs can target environments, which record deployment history per environment, and GitLab environments show which commit and pipeline last deployed to each one. With this in place, "what changed before the incident?" becomes a comparison between two commits instead of a hunt through chat logs.
# What changed between the last two production deployments?
git log --oneline v2.3.1..v2.4.0
git diff --stat v2.3.1..v2.4.0
13. Roll back through version control
Rollbacks should use the same path as any other change. For applications, redeploy the previous tagged artifact, or git revert the bad commit on main and let the pipeline ship the result. For GitOps-managed clusters, revert the commit in the configuration repository and let the agent reconcile. Manual fixes on servers fix the symptom and leave Git out of date, which causes the next deployment to undo them.
GitOps: Git as the desired state
14. Use GitOps to deliver to Kubernetes and other declarative platforms
GitOps extends version control from "what we built" to "what should be running". The OpenGitOps project defines four principles: the desired state is declarative, it is versioned and immutable, it is pulled automatically by software agents, and it is continuously reconciled. In practice a tool such as Argo CD or Flux watches a configuration repository and applies changes to the cluster, and reports drift when someone changes the cluster by hand.
A common layout keeps application source and deployment configuration in separate repositories. CI builds and pushes the image, then opens a pull request in the configuration repository to bump the image tag; merging that request is the deployment. Read what GitOps is and how it improves infrastructure automation for tool details.
Security, compliance and audit
15. Keep secrets out of every repository
Secrets committed to Git spread to every clone, fork and CI cache. Store them in a secrets manager (for example HashiCorp Vault or a cloud provider's secret service) and inject them at runtime. Add prevention at several layers: a local pre-commit scanner, GitHub push protection or GitLab secret push protection (an Ultimate feature), and secret detection in CI. If a secret does leak, rotate it first; history cleanup comes later. See best practices for storing secrets in Git repositories.
16. Give pipelines short-lived, least-privilege credentials
Long-lived cloud keys stored as CI secrets are a common route to a breach. GitHub Actions supports OpenID Connect, so workflows request temporary tokens from AWS, Azure, Google Cloud or HashiCorp Vault at run time instead of storing static credentials. GitLab CI/CD offers ID tokens (the id_tokens keyword) for the same purpose. Scope each credential to one repository, branch or environment, and give the pipeline's built-in token only the permissions the job needs.
# GitHub Actions: allow the job to request an OIDC token, nothing more
permissions:
id-token: write
contents: read
17. Protect branches and tags, and sign what you release
Protected branches stop anyone, including administrators if you configure it, from pushing straight to production code. Add required reviews, required checks and blocked force pushes, and protect release tags. Commit and tag signing (GPG, SSH or X.509) adds evidence of who produced a change; GitHub can require signed commits on a branch, and GitLab Premium push rules can reject unsigned commits. Start with release tags and the default branch.
18. Use the history as your audit trail and delivery data
When every change goes through a reviewed request, Git and the hosting platform already record who wrote it, who approved it, which checks passed and when it was deployed. That record answers most change-management and audit questions without a separate spreadsheet. The same data feeds delivery metrics: DORA defines change lead time as the time from a change being committed to version control until it is deployed to production, alongside deployment frequency, failed deployment recovery time, change fail rate and deployment rework rate. Keep history intact (no rewriting of shared branches) and retain it according to your compliance needs.
Where these practices map to the toolchain
| Practice | GitHub | GitLab |
|---|---|---|
| Required checks before merge | Require status checks to pass | Pipelines must succeed |
| Path ownership | CODEOWNERS + code owner review | CODEOWNERS + code owner approval (Premium) |
| Commit ID in jobs | GITHUB_SHA | CI_COMMIT_SHA |
| Deployment history | Environments and deployments | Environments |
| Short-lived cloud credentials | OIDC (id-token: write) | ID tokens |
| Secret blocking on push | Push protection | Secret push protection (Ultimate); "Prevent pushing secret files" push rule (Premium) |
| Signed commits | Require signed commits | Reject unsigned commits (Premium) |
Practise the full flow
These practices are easier to learn together on one project: commit, open a merge request, watch the pipeline build an image tagged with the SHA, and promote it with GitOps. Our DevOps training and GitLab training cover this end to end, in Kharadi, Pune or live online across India. For the commands behind these steps, see 25 Git commands every DevOps engineer must know, and for interview preparation, our Git and GitHub interview questions.
Frequently asked questions
Why is version control important in DevOps?
It is the single record of every change to code, infrastructure, pipelines and configuration. CI/CD pipelines start from it, reviews happen in it, deployments are traced back to it, and rollbacks use it.
What should be stored in version control in a DevOps team?
Application code, tests, infrastructure as code, pipeline definitions, Kubernetes manifests, environment configuration, database migrations and policies. Secrets, Terraform state, build output and dependencies should stay out.
Should Terraform state be committed to Git?
No. State can contain sensitive values and changes on every apply. Store it in a remote backend with locking and encryption, and keep *.tfstate in .gitignore.
What is the difference between version control and GitOps?
Version control records changes. GitOps uses that record as the desired state of a running system: an agent such as Argo CD or Flux pulls the declared state from Git and continuously reconciles the platform to match it.
How does version control support rollbacks?
Every release is a tagged commit, so you can redeploy the previous tag or revert the faulty commit and let the pipeline ship it. With GitOps, reverting the configuration commit rolls the cluster back.
Which DORA metric depends directly on version control?
Change lead time, which DORA defines as the time from a change being committed to version control until it is deployed in production. Commit and deployment records together give you this number.
How do I keep CI/CD credentials out of Git?
Use a secrets manager and short-lived credentials. GitHub Actions supports OIDC to AWS, Azure, Google Cloud and Vault, and GitLab CI/CD provides ID tokens, so no long-lived cloud keys need to be stored.
Is trunk-based development required for DevOps?
Not strictly, but DORA's research links it to better delivery performance. At minimum, keep branches short-lived and integrate to the main branch at least daily.
References
- DORA: Software delivery performance metrics
- DORA: Trunk-based development capability
- OpenGitOps: GitOps principles v1.0.0
- Semantic Versioning 2.0.0
- Conventional Commits 1.0.0
- HashiCorp: Sensitive data in Terraform state
- GitHub Docs: OpenID Connect in GitHub Actions
- GitHub Docs: About protected branches
- GitHub Docs: About push protection
- GitLab Docs: Predefined CI/CD variables
- GitLab Docs: Environments
- GitLab Docs: OpenID Connect (OIDC) authentication using ID tokens
- GitLab Docs: Secret push protection
- GitLab Docs: Push rules
- Argo CD documentation
What's Your Reaction?
Like
0
Dislike
0
Love
0
Funny
0
Angry
0
Sad
0
Wow
0