Day 3: Branching Strategies, Branch Protection, and CI Rules
In DevOps, a pipeline does not run on an isolated schedule; instead, it is triggered by actions taken within the Git repository. The branching strategy you choose directly dictates when your pipelines run, what stages they execute, and how code is deployed to production. This guide covers the core branching strategies (Git Flow and Trunk-Based Development), branch protection mechanisms, and how to write conditional CI/CD rules in GitLab.
1. Why Branching is the Foundation of CI/CD
Before writing a single line of pipeline configuration, you must understand that branching strategy and CI/CD design are two sides of the same coin. Every pipeline execution is triggered by a Git event—most commonly, a code push or a merge request to a specific branch.
Your branching strategy directly controls three critical pipeline behaviors:
The Risks of Operating Without a Branching Strategy
If a team does not establish and enforce a clear branching strategy, the entire CI/CD workflow breaks down:
- Accidental Deployments: Every push, regardless of completeness, can trigger a production deployment. Half-finished features or experimental code can easily slip into production.
- Unstable Environments: Team members will not know which branch contains stable, deployable code.
- Severe Merge Conflicts: Developers working in isolation for too long without a structured merging cadence will face massive, difficult-to-resolve merge conflicts.
- Production Outages: Without a gatekeeping mechanism, broken code will inevitably reach production, causing application downtime.
2. The Git Flow Branching Strategy
Historically, teams worked out of a single branch (often main or master). When multiple developers pushed code simultaneously, unfinished features frequently broke the build. To solve this, the Git Flow strategy was introduced.
Git Flow is a structured, branch-heavy model where every type of work has a dedicated, isolated branch.
Git Flow Branch Taxonomy
3. Deep Dive: Release Branches vs. Hotfix Branches
Because both release/* and hotfix/* branches eventually merge back into main and develop, they are frequently confused. However, their origins, triggers, and lifecycles are fundamentally different.
4. Trunk-Based Development & Feature Flags
While Git Flow is highly structured, it can slow down delivery due to long-lived feature branches and complex merge paths. Trunk-Based Development is an alternative strategy optimized for high-velocity teams.
In Trunk-Based Development, all developers commit to a single central branch (the "trunk", usually main or master). Feature branches are extremely short-lived—typically lasting less than 24 to 48 hours—before being merged back into the trunk.
The Role of Feature Flags
If developers are constantly merging code into the trunk daily, how do we prevent incomplete features from breaking production? The answer is Feature Flags.
Conceptually, a Feature Flag is an conditional "on/off switch" embedded directly in the application code:
# Conceptual implementation of a Feature Flag
if feature_flags.is_enabled("new_dashboard_v2"):
render_new_dashboard()
else:
render_old_dashboard()
Real-Life Analogy: The Netflix Homepage
Imagine Netflix is building a brand-new homepage. The project is massive and will take weeks to complete.
- Under Git Flow: Developers work on a
feature/new-homepagebranch for 4 weeks. During this time, the branch drifts far behinddevelop, leading to a massive merge conflict when they finally try to merge it.- Under Trunk-Based Development: The work is broken down into tiny, daily tasks (e.g.,
feature/homepage-api,feature/homepage-css). Developers merge these pieces intomainevery single day. Even though the new homepage is only 20% complete on Day 3, it is safely deployed to production because the Feature Flag for the new homepage is set toOFF. Users continue to see the old homepage. Once 100% of the code is merged and verified, the product team toggles the flag toONinstantly, making the new homepage visible without requiring a new code deployment.
Key Benefits of Trunk-Based Development
- Trivial Rollbacks: If a newly released feature causes issues, you do not need to redeploy or revert commits. You simply toggle the Feature Flag to
OFF. - No Merge Hell: Merging daily prevents branches from drifting apart, eliminating complex merge conflicts.
- Continuous Integration: Code is continuously integrated and tested against the true state of the application.
5. Branch Protection & Merge Request Workflows
Because production deployments are automated via CI/CD, protecting your stable branches (main or master) is a critical security requirement. You must prevent developers from pushing code directly to these branches without review.
Enforcing the Merge Request (MR) Workflow
To maintain code quality, all changes destined for protected branches must go through a Merge Request (equivalent to a Pull Request in GitHub).
Developer Local Machine
|
| git push origin feature/add-login
v
GitLab Feature Branch (feature/add-login)
|
| Create Merge Request (MR)
v
Code Review & Pipeline Execution
|
| Approvals Granted & CI Pipeline Passes
v
Merged into Protected Branch (master)
Recommended Branch Protection Settings
To secure a production branch, configure the following settings in GitLab (Settings > Repository > Protected Branches):
- Allowed to Push: Set to No one. This completely blocks direct command-line pushes (e.g.,
git push origin master). - Allowed to Merge: Set to Maintainers (or specific senior roles).
- Required Approvals: Require at least 1 or 2 authorized team members to review and approve the MR before it can be merged.
- Pipeline Success Enforce: Block merges if the CI/CD pipeline fails. This ensures broken code never reaches the protected branch.
- Resolved Discussions: Require all review comments and discussions to be marked as resolved before merging.
- Block Force Push: Ensure
git push --forceis disabled for everyone on protected branches to prevent history deletion.
6. Step-by-Step Lab: Setting Up SSH Authentication & Local Git Workflow
This hands-on lab demonstrates how to configure SSH keys for secure GitLab authentication, clone a repository, and work with local branches.
Step 1: Generate an SSH Key Pair
Open your local terminal and generate a secure SSH key pair:
ssh-keygen -t ed25519 -C "your_email@example.com"
Press Enter to accept the default file path and bypass the passphrase prompt.
Step 2: Add the Public Key to GitLab
- View and copy your public key:
BASH
cat ~/.ssh/id_ed25519.pub - Log in to GitLab.
- Click your profile picture in the top-left/top-right corner and select Preferences.
- On the left sidebar, click SSH Keys.
- Click Add new key, paste your copied public key into the text box, and click Add key.
Step 3: Clone the Repository
Navigate to your working directory and clone your GitLab repository using the SSH URL:
git clone git@gitlab.com:your-username/your-project.git
cd your-project
Step 4: Create a Local Feature Branch
Create and switch to a new feature branch to simulate adding a login feature:
git checkout -b feature/add-login
Step 5: Commit and Push Changes
Create a dummy file, commit it, and push the branch to GitLab:
echo "Login Page Code" > login.txt
git add login.txt
git commit -m "feat: add login page text file"
git push origin feature/add-login
7. Step-by-Step Lab: Enforcing Branch Protection & Testing Direct Push Blocks
Now, we will configure branch protection in the GitLab UI and verify that direct pushes to the protected branch are blocked.
Step 1: Configure Protection in GitLab UI
- In your GitLab project, navigate to Settings > Repository.
- Expand the Protected branches section.
- Select your default branch (e.g.,
masterormain). - Set Allowed to merge to
Maintainers. - Set Allowed to push to
No one. - Click Protect (or update the existing rule).
Step 2: Attempt a Direct Push to the Protected Branch
Switch back to your local master branch, create a change, and attempt to push it directly:
# Switch to master
git checkout master
# Pull latest changes to sync local repository
git pull origin master
# Create a direct change
echo "Direct change to master" > test.txt
git add test.txt
git commit -m "force: direct commit to master"
# Attempt to push directly
git push origin master
Expected Output
GitLab will reject the push and return an error message similar to this:
This confirms that your branch protection rules are active and working. To merge these changes, you must push them to a feature branch and open a Merge Request.
8. Step-by-Step Lab: Configuring CI Rules in .gitlab-ci.yml
In this section, we will write a GitLab CI/CD pipeline that uses conditional rules. We want our testing and building jobs to run on every branch, but our deployment job must only execute when changes are merged into the master branch.
The Evolution of the Pipeline Configuration
1. The Naive Approach (No Rules)
In this basic configuration, every single job runs on every single push, regardless of the branch. This means a push to a temporary feature branch will trigger an unwanted deployment.
stages:
- build
- test
- deploy
build_job:
stage: build
script:
- echo "Building the application..."
unit_test_job:
stage: test
script:
- echo "Running unit tests..."
deploy_job:
stage: deploy
script:
- echo "Deploying application to production..."
2. The Decoupled Production Approach (With CI Rules)
To prevent accidental deployments, we introduce the rules block. We use the predefined GitLab CI/CD variable $CI_COMMIT_BRANCH to evaluate the branch name.
Create or edit your .gitlab-ci.yml file with the following configuration:
stages:
- build
- test
- deploy
build_job:
stage: build
script:
- echo "Building the application artifacts..."
unit_test_job:
stage: test
script:
- echo "Running unit tests..."
lint_test_job:
stage: test
script:
- echo "Running code linter..."
deploy_job:
stage: deploy
script:
- echo "Deploying application to production environment..."
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
when: on_success
Note: Predefined variables like
$CI_COMMIT_BRANCHcontain the name of the branch being built. The ruleif: '$CI_COMMIT_BRANCH == "master"'ensures that thedeploy_jobis only added to the pipeline when the commit occurs on themasterbranch.
9. Complete Pipeline Flow Walkthrough
Here is how the pipeline behaves depending on which branch receives the commit.
Scenario A: Developer pushes to feature/add-login
Scenario B: Merge Request is approved and merged into master
10. Summary and Key Takeaways
- CI/CD and Branching are Interlinked: Your branching strategy dictates your pipeline's execution path, environment targets, and overall stability.
- Git Flow is Structured: It uses long-lived
developandmainbranches alongside short-livedfeature/*,release/*, andhotfix/*branches to isolate work. - Trunk-Based Development is Fast: It relies on developers merging to the main branch daily, using Feature Flags to keep incomplete features hidden from users.
- Branch Protection is Mandatory: Protecting your production branch (
master/main) by blocking direct pushes ensures all code is reviewed via Merge Requests. - CI Rules Prevent Disasters: Using the
rulesblock in.gitlab-ci.ymlwith variables like$CI_COMMIT_BRANCHallows you to restrict critical jobs (like deployments) to stable branches. - Feature Flags Enable Decoupled Releases: They separate the concept of deploying code to production from releasing features to end-users, providing a safety net for rapid development.