Building Your First CI/CD Pipeline with GitHub Actions and Azure
A practical walkthrough of automated deployments that you can actually use in production.
CI/CD sounds intimidating until you build your first pipeline. Then it just makes sense.
I'm going to walk you through setting up automated deployments from GitHub to Azure — the same pattern I use for real projects.
What We're Building
A workflow that:
- Triggers when you push to the main branch
- Builds and tests your application
- Deploys to Azure App Service
- Notifies you of success or failure
No magic. No complex abstractions. Just working automation.
Prerequisites
- A GitHub repository with some code
- An Azure subscription
- An Azure App Service (or we'll create one)
Step 1: Connect GitHub to Azure
The secure way to connect GitHub Actions to Azure is with a service principal and federated credentials. No secrets to rotate.
# Create a service principal
az ad sp create-for-rbac --name "github-actions-sp" \
--role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
Save that JSON output — you'll need it.
In your GitHub repo, go to Settings → Secrets and variables → Actions and add:
AZURE_CREDENTIALS: The JSON output from aboveAZURE_SUBSCRIPTION_ID: Your subscription ID
Step 2: The Basic Workflow
Create .github/workflows/deploy.yml:
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AZURE_WEBAPP_NAME: your-app-name
AZURE_WEBAPP_PACKAGE_PATH: '.'
NODE_VERSION: '20.x'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-build
path: ./dist
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: app-build
path: ./dist
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to App Service
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
package: ./dist
Understanding the Workflow
Let me break down what's happening:
Triggers
on:
push:
branches: [main]
pull_request:
branches: [main]
This runs on every push to main AND on PRs targeting main. PRs will run tests but won't deploy (notice the if condition on the deploy job).
Build Job
The build job runs on every trigger:
- Checks out your code
- Sets up Node.js with caching (faster subsequent runs)
- Installs dependencies with
npm ci(clean install, more reliable thannpm install) - Runs tests
- Builds the application
- Uploads the build as an artifact
Deploy Job
The deploy job only runs when:
- The build job succeeds (
needs: build) - We're on the main branch (
if: github.ref == 'refs/heads/main')
This means PRs get tested but not deployed. Only merged code goes to production.
Step 3: Add Environment Protection
For production deployments, you probably want approval gates.
In GitHub: Settings → Environments → New environment
Create "production" and add:
- Required reviewers
- Wait timer (optional)
- Deployment branches (only main)
Update your workflow:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment: production # Add this line
Now deployments will wait for approval.
Step 4: Add Notifications
Nobody wants to babysit pipelines. Add Slack or Teams notifications:
- name: Notify on success
if: success()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-type: application/json' \
-d '{"text":"✅ Deployment to production succeeded!"}'
- name: Notify on failure
if: failure()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-type: application/json' \
-d '{"text":"❌ Deployment to production failed!"}'
Common Gotchas
1. Secrets Not Available in PRs from Forks
For security, GitHub doesn't expose secrets to workflows triggered by PRs from forks. Your tests should work without Azure credentials.
2. Caching Saves Time
The cache: 'npm' in the setup-node action caches node_modules. First run might take 2 minutes. Subsequent runs take 30 seconds.
3. Artifact Retention
By default, artifacts are kept for 90 days. For frequent deployments, you might want to reduce this:
- uses: actions/upload-artifact@v4
with:
name: app-build
path: ./dist
retention-days: 7
The Bigger Picture
This workflow is a starting point. Real-world pipelines often add:
- Multiple environments (dev → staging → production)
- Database migrations
- Smoke tests after deployment
- Rollback capabilities
- Infrastructure as Code deployment
But don't overcomplicate it initially. Get the basic flow working, then iterate.
Why This Matters
Manual deployments are:
- Slow
- Error-prone
- Not auditable
- Dependent on specific people being available
Automated deployments are:
- Fast (minutes, not hours)
- Consistent (same process every time)
- Auditable (full history in GitHub)
- Available (push code, it deploys)
Once you experience automated deployments, you'll never want to go back to manual.
Start simple. Build from there.
Read Next
Infrastructure as Code: Why ARM Templates Are Still Worth Learning in 2025
Bicep and Terraform get all the hype, but understanding ARM templates will make you a better cloud engineer.
Implementing Conditional Access for Azure Virtual Desktop
A step-by-step guide to securing your AVD environment with Conditional Access policies that actually make sense.
Building AI Solutions with Azure AI Foundry and Copilot Studio
A hands-on technical guide to building production AI applications using Azure AI Foundry, prompt flows, and Copilot Studio.