Tuesday, 16 December 2025

Zero-Downtime CI/CD to Azure App Service with GitHub Actions and Slots

 This guide walks you through building a production-grade, zero-downtime CI/CD pipeline using GitHub Actions and Azure App Service deployment slots.

The problem we’re solving

Technologies and services used

High-level CI/CD architecture

CI/CD flow diagram (Mermaid)

Press enter or click to view image in full size

The Core Workflow (Sanitized Example)

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: inherit

Jobs and steps overview

No comments:

Post a Comment