Modern Trunk-Based Development
The Real-World Problem
What happens when you deploy code manually?
This is the problem CI/CD pipelines solve.
| Without CI/CD | With CI/CD |
|---|---|
| Manual deployments take hours | Automated deployments in minutes |
| Tests run inconsistently | Every commit automatically tested |
| "Works on my machine" problems | Consistent environments everywhere |
| Deployments fail most of the time | Deployments are successful most of the time |
| Rollbacks are manual and risky | Automatic rollbacks on failure |
Automatically test and integrate code changes frequently
Automatically deploy code that passes all tests
Together: A pipeline that automatically builds, tests, and deploys your code
Key Point: Main branch is always production-ready. No separate staging environments.
From Pull Request to Production
CI runs on PR, CD runs when PR is merged to main
The Trigger
git checkout -b feature/new-logingit push origin feature/new-loginWhy do you think we test BEFORE merging to main?
We catch issues early and keep main branch always deployable!
# Developer workflow (trunk-based)
$ git checkout -b feature/fix-auth
$ git add .
$ git commit -m "Fix user authentication bug"
$ git push origin feature/fix-auth
# Create Pull Request on GitHub/GitLab
→ PR targets main branch
→ CI pipeline automatically triggers
→ Tests run, Docker images build (frontend & backend, but not pushed)
→ If all pass, PR can be merged
→ After merge to main: CD pipeline builds, pushes to ACR, and deploys to Azure Container Apps
Key Point: No direct pushes to main. Everything goes through PRs for review and automated testing.
Quality Assurance
If tests pass: Pipeline continues to Build
If tests fail: Pipeline stops, developer gets notified
# GitHub Actions workflow - runs on PRs
name: CI Pipeline
on:
pull_request:
branches: [main] # Only on PRs targeting main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Run unit tests
run: npm test
- name: Run linting
run: npm run lint
- name: Security scan
run: npm audit
Why this matters: Catch bugs before they merge to main. Main branch stays clean!
Creating the Artifact
Key Point: Build on PR to verify, but only push images to ACR when code is merged to main
We don't want Docker images from branches that might never be merged!
# Build job on PR - only builds, doesn't push
jobs:
build:
needs: test # Only runs if tests pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build frontend image (verify it works)
run: docker build -t frontend-app:${{ github.sha }} ./frontend
- name: Build backend image (verify it works)
run: docker build -t backend-app:${{ github.sha }} ./backend
# No push on PR - we don't want images from unmerged branches
Key Point: Build on PR to verify it works, but don't push images until code is merged to main!
Going Live
Why do we only deploy when code is merged to main?
Main branch is the source of truth. If it's in main, it's tested and ready for production!
# Deploy job - only runs when PR is merged to main
on:
push:
branches: [main] # Only on merge to main
jobs:
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy frontend to Azure Container Apps
uses: azure/aci-deploy@v1
with:
resource-group: my-resource-group
container-app-name: frontend-app
image: ${{ secrets.ACR_ADDR }}/frontend-app:${{ github.sha }}
- name: Deploy backend to Azure Container Apps
uses: azure/aci-deploy@v1
with:
resource-group: my-resource-group
container-app-name: backend-app
image: ${{ secrets.ACR_ADDR }}/backend-app:${{ github.sha }}
- name: Health check
run: |
sleep 10
curl -f ${{ secrets.FRONTEND_URL }} || exit 1
curl -f ${{ secrets.BACKEND_URL }}|| exit 1
Safety First: Automatic health checks and rollbacks protect production. Main branch = production!
A Complete Pipeline Example
# CI Pipeline - runs on Pull Requests
name: CI Pipeline
on:
pull_request:
branches: [main]
jobs:
# Test on PR
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
- run: npm run lint
# Build on PR (verify it works, but don't push)
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build frontend image (verify)
run: docker build -t frontend-app:${{ github.sha }} ./frontend
- name: Build backend image (verify)
run: docker build -t backend-app:${{ github.sha }} ./backend
# No push - we don't want images from unmerged branches
---
# CD Pipeline - runs when PR is merged to main
name: CD Pipeline
on:
push:
branches: [main]
jobs:
# Build and push images to Azure Container Registry (only on merge)
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Azure Container Registry
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.ACR_ADDR }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push frontend image
run: |
docker build -t ${{ secrets.ACR_ADDR }}/frontend-app:${{ github.sha }} ./frontend
docker tag ${{ secrets.ACR_ADDR }}/frontend-app:${{ github.sha }} ${{ secrets.ACR_ADDR }}/frontend-app:latest
docker push ${{ secrets.ACR_ADDR }}/frontend-app:${{ github.sha }}
docker push ${{ secrets.ACR_ADDR }}/frontend-app:latest
- name: Build and push backend image
run: |
docker build -t ${{ secrets.ACR_ADDR }}/backend-app:${{ github.sha }} ./backend
docker tag ${{ secrets.ACR_ADDR }}/backend-app:${{ github.sha }} ${{ secrets.ACR_ADDR }}/backend-app:latest
docker push ${{ secrets.ACR_ADDR }}/backend-app:${{ github.sha }}
docker push ${{ secrets.ACR_ADDR }}/backend-app:latest
# Deploy to Azure Container Apps
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy frontend to Azure Container Apps
uses: azure/container-apps-deploy-action@v1
with:
acrName: ${{ secrets.ACR_NAME }}
containerAppName: frontend-app
resourceGroup: ${{ secrets.AZURE_RG }}
imageToDeploy: ${{ secrets.ACR_ADDR }}/frontend-app:${{ github.sha }}
- name: Deploy backend to Azure Container Apps
uses: azure/container-apps-deploy-action@v1
with:
acrName: ${{ secrets.ACR_NAME }}
containerAppName: backend-app
resourceGroup: ${{ secrets.AZURE_RG }}
imageToDeploy: ${{ secrets.ACR_ADDR }}/backend-app:${{ github.sha }}
- name: Health check
run: |
sleep 15
curl -f ${{ secrets.FRONTEND_URL }} || exit 1
curl -f ${{ secrets.BACKEND_URL }} || exit 1
Key Point: CI runs on PRs (test and verify build), CD runs on merge (build, push to ACR, and deploy to Azure Container Apps). Only merged code gets pushed to registry!
Why do you think trunk-based development (one main branch) is better than having staging/dev branches?
Less complexity, faster feedback, and main is always production-ready!
What You've Learned
Remember: Modern CI/CD = PR-based workflow + trunk-based development. Keep it simple!