Shipping Faster with Automated Pipelines: CI/CD with GitHub Actions
Every manual deployment is a deployment that will eventually be forgotten or done wrong. This is the GitHub Actions setup we use to build, test, and ship to Azure on every merge — with caching strategies that make pipelines actually fast.
GitHub Actions automates software workflows directly in your repository. This guide covers CI/CD fundamentals, workflow structure, and production patterns for building, testing, and deploying applications.
What is CI/CD?
CI/CD is a set of practices that automate the software delivery process. It ensures code changes are automatically tested, validated, and deployed with minimal manual intervention.
CI/CD PIPELINE OVERVIEW
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Code │ │ Build │ │ Test │ │ Deploy │
│ Push │───▶│ Stage │───▶│ Stage │───▶│ Stage │
│ │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
Trigger Compile Run Tests Production
Pipeline Assets Lint Code Staging
Bundle Type Check Review
Continuous Integration (CI)
CI automatically builds and tests code changes when developers push to the repository. It catches bugs early by validating every commit.
- Automated builds: Compile and bundle code on every push
- Automated tests: Run unit, integration, and end-to-end tests
- Code quality: Lint, format, and type-check code
- Fast feedback: Developers know within minutes if changes broke something
Continuous Delivery (CD)
CD extends CI by automatically deploying validated code to staging or production environments. The difference between Delivery and Deployment:
- Continuous Delivery: Code is always deployable, but requires manual approval
- Continuous Deployment: Every validated change deploys automatically to production
GitHub Actions Fundamentals
GitHub Actions is a CI/CD platform built into GitHub. Workflows are defined in YAML files and triggered by repository events.
GITHUB ACTIONS ARCHITECTURE
┌─────────────────────────────────────────────────────────────┐
│ Repository │
│ │
│ .github/workflows/ │
│ ├── ci.yml ──▶ Triggered on pull_request │
│ └── cd.yml ──▶ Triggered on push to main │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GitHub Runners │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ubuntu │ │ windows │ │ macos │ │
│ │ latest │ │ latest │ │ latest │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Workflow Structure
A workflow consists of triggers, jobs, and steps. Jobs run on runners (virtual machines), and steps execute commands or actions.
name: Workflow Name
on: # Triggers
push:
branches: [main]
pull_request:
branches: [main]
jobs:
job-name: # Job definition
runs-on: ubuntu-latest # Runner
steps: # Sequential steps
- name: Step Name
uses: action@version # Use an action
- name: Run Command
run: npm test # Run shell command
Workflow Triggers
Workflows are triggered by events in your repository.
on:
# Push to specific branches
push:
branches: [main, develop]
# Pull request events
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# After another workflow completes
workflow_run:
workflows: [CI]
types: [completed]
Environment Variables and Secrets
Secrets store sensitive data. Environment variables configure workflow behavior.
jobs:
deploy:
runs-on: ubuntu-latest
env:
# Workflow-level variables
NODE_ENV: production
REGISTRY: ${{ secrets.CONTAINER_REGISTRY }}
steps:
- name: Deploy
env:
# Step-level variables
API_KEY: ${{ secrets.API_KEY }}
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Deploying to $NODE_ENV"
./deploy.sh
CI Workflow Pattern
A CI workflow validates code on every pull request. It builds the project, runs tests, and reports results.
CI WORKFLOW FLOW
Pull Request
│
▼
┌────────────────────────────────────────────────────────────────┐
│ CI Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Checkout │──▶│ Install │──▶│ Lint │──▶│ Build │ │
│ │ Code │ │ Deps │ │ Code │ │ Project │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Test │ │
│ │ Suite │ │
│ └──────────┘ │
│ │ │
└────────────────────────────────────────────────────┼───────────┘
▼
✓ Merge Allowed
✗ Block Merge
Complete CI Workflow
name: CI
on:
pull_request:
branches:
- main
jobs:
build:
name: Build and Test
timeout-minutes: 15
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_REMOTE_ONLY: true
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Cache turbo build setup
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run test
Caching with Turborepo
Turborepo's remote cache shares build artifacts across CI runs and developers. When one run builds a package, subsequent runs skip rebuilding it.
- TURBO_TOKEN: Authentication for Vercel remote cache
- TURBO_TEAM: Team or organization identifier
- TURBO_REMOTE_ONLY: Only use remote cache, skip local
- actions/cache: Caches the
.turbofolder for faster restores
CD Workflow Pattern
A CD workflow deploys validated code to production. It builds Docker images, pushes to a registry, and updates container services.
CD WORKFLOW FLOW
Push to Main
│
▼
┌──────────────────────────────────────────────────────────────┐
│ CD Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Checkout │──▶│ Build │──▶│ Push │──▶│ Deploy │ │
│ │ Code │ │ Docker │ │ Registry │ │ ACA │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Tag with │ │ Restart │ │
│ │ Commit SHA │ │ Services │ │
│ └────────────┘ └────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Complete CD Workflow
name: CD
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy to Azure
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
AZURE_CONTAINER_REGISTRY: ${{ secrets.AZURE_CONTAINER_REGISTRY }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Azure Login
uses: azure/login@v2
with:
creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}'
- name: Build Docker Image
run: |
docker build --platform=linux/amd64 \
-t my-service \
-f apps/my-service/Dockerfile .
docker tag my-service \
${{ env.AZURE_CONTAINER_REGISTRY }}/my-service:${{ github.sha }}
- name: Login to ACR
run: |
az acr login --name ${{ env.REGISTRY_USERNAME }}
- name: Push to Registry
run: |
docker push ${{ env.AZURE_CONTAINER_REGISTRY }}/my-service:${{ github.sha }}
- name: Deploy to Container Apps
run: |
az containerapp update \
--name my-service \
--resource-group my-project \
--image ${{ env.AZURE_CONTAINER_REGISTRY }}/my-service:${{ github.sha }}
Initial Deployment vs Updates
Use az containerapp create for initial deployment, then az containerapp update for subsequent deployments.
# Initial deployment with full configuration
- name: Create Container App
run: |
az containerapp create \
--name my-service \
--resource-group my-project \
--environment my-environment \
--image ${{ env.REGISTRY }}/my-service:${{ github.sha }} \
--target-port 3000 \
--ingress external \
--min-replicas 1 \
--max-replicas 10 \
--env-vars \
DATABASE_URL=${{ secrets.DATABASE_URL }} \
REDIS_HOST=${{ secrets.REDIS_HOST }}
# Subsequent deployments (update image only)
- name: Update Container App
run: |
az containerapp update \
--name my-service \
--resource-group my-project \
--image ${{ env.REGISTRY }}/my-service:${{ github.sha }}
Image Tagging Strategies
Proper image tagging enables traceability and rollback.
- ${{ github.sha }}: Unique commit hash for exact version tracking
- latest: Always points to the most recent build
- v1.0.0: Semantic versioning for releases
- ${{ github.ref_name }}: Branch name for environment-specific builds
- name: Tag Image
run: |
# Commit SHA (recommended for traceability)
docker tag my-service $REGISTRY/my-service:${{ github.sha }}
# Latest tag
docker tag my-service $REGISTRY/my-service:latest
# Branch-based tag
docker tag my-service $REGISTRY/my-service:${{ github.ref_name }}
Best Practices
Security
- Never hardcode secrets: Use GitHub Secrets for sensitive values
- Least privilege: Service principals should have minimal required permissions
- Pin action versions: Use commit SHAs instead of tags for third-party actions
- Audit workflows: Review workflow changes in pull requests
Performance
- Cache dependencies: Reduce install time with actions/cache
- Remote caching: Share build artifacts across runs with Turborepo
- Parallel jobs: Run independent jobs concurrently
- Timeout limits: Set reasonable timeouts to fail fast
Reliability
- Idempotent deployments: Running the same workflow twice should produce the same result
- Rollback strategy: Tag images with commit SHA for easy rollback
- Health checks: Verify deployments succeeded before marking complete
- Notifications: Alert on failures via Slack, email, or GitHub issues
Maintainability
- Descriptive names: Use clear job and step names for debugging
- Documentation: Comment complex workflow logic
- Version control: Workflows are code—review them like code
Conclusion
GitHub Actions provides a complete CI/CD platform integrated directly into your repository. CI workflows validate every change with automated builds and tests, catching bugs before they reach production. CD workflows automate deployment, reducing manual errors and enabling frequent releases.
The combination of Turborepo's remote caching and GitHub Actions' built-in caching significantly reduces build times. For containerized applications, the workflow from code push to container registry to production deployment becomes fully automated and traceable through commit-based image tags.
Whether deploying a single service or orchestrating multiple microservices, these patterns scale from simple projects to complex production systems.
Related Reading
These posts extend the CI/CD concepts covered here into containerisation and cloud deployment:
- From Code to Container: A Production Docker Guide — the multi-stage Docker builds and Compose orchestration that feed the GitHub Actions pipelines described above.
- How We Deployed and Scaled on Azure: A Production Playbook — how the GitHub Actions CD workflow connects to Azure Container Apps to complete the zero-manual-step deployment pipeline.
See how these CI/CD patterns are applied in the infrastructure projects built by Brahim Boumlik.
Written by

Technical Lead and Full Stack Engineer leading a 5-engineer team at Fygurs (Paris, Remote) on Azure cloud-native SaaS. Graduate of 1337 Coding School (42 Network / UM6P). Writes about architecture, cloud infrastructure, and engineering leadership.