This guide walks you through building a production-grade, zero-downtime CI/CD pipeline using GitHub Actions and Azure App Service deployment slots.
- Who this is for: Engineers new to CI/CD, platform engineers, and app teams onboarding to GitHub Actions + Azure.
- What you’ll get: A clear mental model, ready-to-adapt YAML, and actionable setup steps.
The problem we’re solving
Shipping web apps quickly without breaking production is hard. Teams need:
- Safe, automated deployments on every change to main.
- Validation in a production-like environment before customers see new code.
- Zero downtime during releases and quick rollbacks if something goes wrong.
This article shows how to build a complete, end-to-end CI/CD pipeline that solves these needs using GitHub Actions and Azure App Service deployment slots.
Technologies and services used
- GitHub Actions (CI/CD orchestration)
- Azure App Service (web hosting)
- Azure App Service Deployment Slots (e.g., staging, production)
- Azure Key Vault (secure secret storage)
- Azure AD Service Principal or OIDC (GitHub → Azure auth)
- Playwright/Cypress (example E2E testing)
- GitHub Environments, Secrets, and Variables
High-level CI/CD architecture
GitHub Actions orchestrates the end-to-end lifecycle:
- Trigger on main branch pushes or manual dispatch.
- Build the web app with environment-specific config and secrets.
- Deploy the build artifact to an Azure App Service slot (e.g., staging).
- Run E2E tests against the staging slot URL.
- If tests pass, swap the slot with production for zero downtime.
Azure App Service slots:
- A slot is like a second, fully running environment under the same App Service.
- You deploy to the non-production slot, warm it up, validate it, then atomically “swap” traffic with production.
- Sticky configuration (“slot settings”) stays with the slot; non-sticky settings follow the code during swap.
CI/CD flow diagram (Mermaid)

The Core Workflow (Sanitized Example)
Below is a trimmed, production-ready pattern you can adapt. It mirrors the structure of a build/deploy/test/swap pipeline and uses slot-based deployment for zero downtime. Replace placeholders and composite action paths with your repo’s structure.
name: Prod - Web App Deployment
on:
push:
branches:
- main
paths:
- 'UI/**'
workflow_dispatch:
inputs:
slot:
description: 'Web App slot to deploy to'
required: false
default: 'staging'
type: choice
options: [staging, production]
runE2E:
description: 'Run E2E tests after deployment'
required: false
default: 'true'
type: choice
options: ['true', 'false']
runMobileE2E:
description: 'Run Mobile E2E tests after deployment'
required: false
default: 'true'
type: choice
options: ['true', 'false']
env:
SLOT_NAME: ${{ inputs.slot || 'staging' }}
jobs:
build:
runs-on: windows-latest
environment: Production
steps:
- uses: actions/checkout@v2
- uses: Azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: Azure/get-keyvault-secrets@v1
id: keyVaultSecrets
with:
keyvault: ${{ secrets.KEYVAULT_NAME }}
secrets: 'SECRET_A,SECRET_B,SECRET_C' # use your own secret names
- name: build
uses: ./.github/actions/build_web
with:
npmuserEmail: ${{ secrets.NPM_USER }}
npmtoken: ${{ secrets.NPM_TOKEN }}
# Example of mapping secrets/vars to your build-time env
appKey: ${{ secrets.APP_KEY }}
appPath: ${{ secrets.API_PATH }}
skipTestRun: "false"
deploy:
runs-on: windows-latest
needs: build
environment:
name: Production
url: ${{ steps.deploy.outputs.webapp-url }}
steps:
- uses: actions/checkout@v2
- uses: Azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: deploy
id: deploy
uses: ./.github/actions/deploy_web
with:
appName: ${{ secrets.AZUREAPPSERVICE_WEBAPP_NAME }}
slotName: ${{ env.SLOT_NAME }}
resourceGroup: ${{ vars.AZURE_RESOURCE_GROUP || secrets.AZURE_RESOURCE_GROUP }}
E2ETests:
needs: deploy
if: ${{ github.event_name != 'workflow_dispatch' || inputs.runE2E == 'true' }}
uses: ./.github/workflows/reusable_e2e_ui_tests.yml
with:
product: ${{ vars.PRODUCT || 'Your Product Name' }}
environment: Production
useStagingUrl: true
secrets: inherit
E2EMobileTests:
needs: E2ETests
if: ${{ github.event_name != 'workflow_dispatch' || inputs.runMobileE2E == 'true' }}
uses: ./.github/workflows/reusable_e2e_mobile_tests.yml
with:
product: ${{ vars.PRODUCT || 'Your Product Name' }}
environment: Production
useStagingUrl: true
secrets: inherit
SwapSlot:
needs: E2ETests
if: ${{
(github.event_name != 'workflow_dispatch' || inputs.runE2E == 'true' || inputs.runMobileE2E == 'true') &&
( !(github.event_name == 'workflow_dispatch' && inputs.runE2E != 'true') || needs.E2ETests.result == 'success' ) &&
( !(github.event_name == 'workflow_dispatch' && inputs.runMobileE2E != 'true') || needs.E2EMobileTests.result == 'success' )
}}
uses: ./.github/workflows/reusable_slot_swap.yml
with:
environment: Production
slot: ${{ inputs.slot || 'staging' }}
secrets: inheritJobs and steps overview
- build: Compiles your app, resolves secrets, and prepares artifacts.
- deploy: Publishes the artifact to the specified Azure App Service slot.
- E2ETests: Executes UI E2E tests against the staging slot URL.
- E2EMobileTests: Runs mobile/webview-focused E2E tests after UI tests.
- SwapSlot: Conditionally swaps staging and production based on test outcomes.
No comments:
Post a Comment