Logo
CloudWithSingh
Back to all posts
DevOps
GitHub Actions
Azure
CI/CD
Automation

Building Your First CI/CD Pipeline with GitHub Actions and Azure

A practical walkthrough of automated deployments that you can actually use in production.

Parveen Singh
October 28, 2025
4 min read

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:

  1. Triggers when you push to the main branch
  2. Builds and tests your application
  3. Deploys to Azure App Service
  4. 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 above
  • AZURE_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 than npm 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