DevOps from Zero to Hero: Version Control for Teams

2026-04-27 | Gabriel Garrido | 6 min read
Share:

Support this blog

If you find this content useful, consider supporting the blog.

Introduction

Welcome to article 3 of the DevOps from Zero to Hero series. Now it is time to talk about one of the most critical tools in any engineering team: version control for teams.


If you are working alone, you can get away with committing directly to main. But the moment a second person touches the same codebase, you need structure, conventions, and guardrails to avoid stepping on each other’s toes. In this article we will cover branching strategies, pull requests, conventional commits, protected branches, and merge strategies.


Why Version Control Matters in DevOps

Version control is the foundation for everything you will build in DevOps:


  • Collaboration Multiple people can work on the same codebase simultaneously without overwriting each other’s changes
  • Auditability Every change is recorded with a timestamp, author, and message. You can trace exactly what changed and who changed it
  • Rollback Made a bad deployment? Revert to a previous known-good state in seconds
  • Automation CI/CD pipelines trigger based on Git events. Without version control, you cannot automate builds, tests, or deployments
  • Code review Pull requests create a structured process for reviewing changes before they reach production

Branching Strategies

A branching strategy defines how your team uses branches to develop, test, and release software. Let’s look at the three most common ones.


Trunk-Based Development

Everyone commits to a single branch (main). Feature branches are short-lived, lasting no more than a day or two.


main ─────●─────●─────●─────●─────●─────●─────
            \       /   \     /
feature-a    ●───●     feature-b

  • Short-lived branches Features are broken into small pieces merged within a day or two
  • Feature flags Incomplete features are hidden behind flags so they can be merged safely
  • Continuous integration Everyone integrates frequently, reducing merge conflicts

This is the strategy I recommend for most teams. Companies like Google and Netflix use it at scale.


GitFlow

Uses multiple long-lived branches: main, develop, feature/*, release/*, and hotfix/*. Designed for teams with scheduled releases.


main     ─────●───────────────────●──────────────
               \                 /
develop   ──●───●───●───●───●───●───●───●────────
              \   /       \       /
feature-a      ●──●        release/1.0

Good for versioned software (libraries, mobile apps), but adds unnecessary complexity for web applications deployed continuously.


GitHub Flow

Simplified workflow: main and short-lived feature branches. Open a PR, get a review, merge. That is it.


  • Simple Only two branch types: main and feature branches
  • PR driven Every change goes through a pull request
  • Deploy from main The main branch is always deployable

For most teams building web applications, use trunk-based development or GitHub Flow. Use GitFlow only if you genuinely need structured release cycles.


Pull Requests: The Heart of Team Collaboration

A pull request is more than a merge request. It is a conversation about the changes you are proposing. Good PRs are the single most important practice for maintaining code quality.


What a Good PR Looks Like

## What
Brief description of what this PR does.

## Why
Why is this change needed? Link to the issue or ticket.

## How
High-level overview of the approach.

## Testing
How was this tested? Include relevant test commands or screenshots.

## Checklist
- [ ] Tests pass locally
- [ ] Documentation updated if needed
- [ ] No breaking changes (or migration path documented)

Review Best Practices

  • Be constructive Suggest improvements, offer alternatives when you disagree
  • Focus on what matters Architecture and correctness over style preferences. Let the linter handle formatting
  • Ask questions “Could you explain why this approach was chosen?” beats “This is wrong”
  • Review promptly Blocked PRs kill momentum. Review within hours, not days
  • Keep PRs small Aim for under 400 lines. Large PRs get rubber-stamped

Conventional Commits

Conventional commits are a specification for structured commit messages that enable automated changelogs and version bumps.


The Format

<type>(<scope>): <description>

[optional body]
[optional footer(s)]

Common types: feat, fix, docs, style, refactor, test, chore, ci, perf.


Examples

feat(auth): add OAuth2 login with GitHub
fix(api): handle null response from payment gateway
docs(readme): update installation instructions for v2
chore(deps): bump phoenix_live_view from 1.0.0 to 1.1.0
refactor(database): extract connection pooling into separate module
ci(github-actions): add Elixir formatter check to PR workflow
feat(notifications)!: redesign notification system

BREAKING CHANGE: notification payloads now use snake_case keys

  • Automated changelogs Tools like release-please generate changelogs from commit history
  • Semantic versioning Commit type determines patch (fix), minor (feat), or major (breaking change)
  • Readable history git log --oneline becomes a clear story of what happened

Protected Branches

Protected branches prevent dangerous actions on important branches like main. Here is how to set them up on GitHub:


  1. Go to your repository, then Settings > Branches
  2. Click Add branch protection rule, set pattern to main
  3. Configure the rules:

[x] Require a pull request before merging
    [x] Require approvals: 1
    [x] Dismiss stale approvals when new commits are pushed

[x] Require status checks to pass before merging
    [x] Require branches to be up to date before merging

[x] Do not allow force pushes
[x] Do not allow deletions
[x] Do not allow bypassing the above settings

You can also do this with the GitHub CLI:

gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --input - <<'EOF'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["ci/tests", "ci/lint"]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "dismiss_stale_reviews": true,
    "required_approving_review_count": 1
  },
  "restrictions": null,
  "allow_force_pushes": false,
  "allow_deletions": false
}
EOF

CODEOWNERS File

Define who reviews changes to specific parts of the codebase:

# .github/CODEOWNERS
* @your-team/backend
/assets/ @your-team/frontend
/terraform/ @your-team/platform
/.github/workflows/ @your-team/platform @your-team/leads

Merge Strategies

When merging a PR on GitHub, you have three options:


Merge Commit preserves all individual commits plus a merge commit. Full history, but can get messy.

# * abc1234 Merge pull request #42
# |\
# | * def5678 fix: handle edge case
# | * ghi9012 feat: add JWT validation
# |/
# * mno7890 Previous commit

Squash and Merge combines all commits into one on main. Clean, linear history.

# * abc1234 feat(auth): add JWT authentication (#42)
# * mno7890 Previous commit

Rebase and Merge replays individual commits on top of main. Linear history with full commit detail.

# * abc1234 fix: handle edge case
# * def5678 feat: add JWT validation
# * ghi9012 feat: add login endpoint
# * mno7890 Previous commit

For most teams, squash and merge is the sweet spot. It keeps main clean and encourages small, focused PRs.


Practical Tips

Meaningful branch names with consistent prefixes:

feature/add-oauth-login
fix/null-pointer-in-payment
chore/upgrade-elixir-to-1.17
docs/update-api-reference

Atomic commits where each commit represents one logical change:

git add lib/auth/session.ex
git commit -m "fix(auth): prevent session fixation on password reset"

git add test/auth/session_test.exs
git commit -m "test(auth): add regression test for session fixation"

Rebase before merging to keep your branch up to date:

git fetch origin
git rebase origin/main
# Use --force-with-lease instead of --force (safer)
git push --force-with-lease origin feature/add-auth

Clean up merged branches to avoid clutter:

git fetch --prune
git branch --merged main | grep -v "main" | xargs git branch -d

Closing notes

Version control is the backbone of how teams collaborate and how code reaches production safely. Getting your Git workflow right early saves enormous amounts of pain down the road.


The key takeaways: use trunk-based development or GitHub Flow, keep PRs small, adopt conventional commits, protect your main branch, and use squash merges. Start simple, add complexity only when you need it.


In the next article, we will dive into CI/CD pipelines. Stay tuned!


Errata

If you spot any error or have any suggestion, please send me a message so it gets fixed.

Also, you can check the source code and examples in the repository here.



$ Comments

Online: 0

Please sign in to be able to write comments.

2026-04-27 | Gabriel Garrido