Skip to content

GitHub Actions CI/CD

Use GitHub Actions when your source code already lives in GitHub and you want one workflow to build, test, authenticate to Azure, and deploy to App Service. This is the most flexible deployment method for teams that need repeatability and gated releases.

Main Content

GitHub Actions Release Flow

flowchart TD
    A[Push or pull request] --> B[Checkout source]
    B --> C[Build and test]
    C --> D[Azure login with OIDC]
    D --> E[Deploy artifact to staging slot]
    E --> F[Smoke test slot]
    F --> G{Approved?}
    G -- Yes --> H[Swap slot to production]
    G -- No --> I[Stop promotion and investigate]

Authentication and Secrets

Microsoft Learn recommends OpenID Connect (OIDC) for GitHub Actions because it avoids long-lived secrets. For an App Service deployment workflow, the usual repository secrets are:

Secret Purpose
AZURE_CLIENT_ID Microsoft Entra application or managed identity client ID used by azure/login.
AZURE_TENANT_ID Tenant ID for the Azure directory.
AZURE_SUBSCRIPTION_ID Subscription that contains the App Service app.

Prefer OIDC over publish profiles

Publish profiles work, but OIDC is the better long-term choice because GitHub exchanges a short-lived token at runtime instead of storing a reusable deployment credential.

Portal view: Deployment Center (GitHub source)

Azure Portal Deployment Center blade for app-test-20251107 Web App with the Settings tab selected and Containers (new), Logs, and FTPS Credentials tabs visible. The toolbar shows Save and Discard (disabled), Refresh, Browse, Sync (disabled), and Send us your feedback. An information banner at the top reads "You are now in the production slot, which is not recommended for setting up CI/CD. Learn more". The body text "Deploy and build code from your preferred source and build provider" precedes a Source dropdown set to GitHub, with "Building with GitHub Actions" and a Change provider link below. A GitHub section explains that App Service places a GitHub Actions workflow in the chosen repository and shows Signed in as demouser with a Change account link, followed by required Organization, Repository, and Branch dropdowns all in empty Select state. The left navigation expands Deployment with Deployment slots and Deployment Center (highlighted) entries.

The Portal's Deployment Center is the click-driven path that this workflow YAML replaces. When you fill in Organization, Repository, and Branch and click Save, Azure generates a workflow file that uses the publish-profile authentication path by default, not the OIDC pattern shown above. The banner "You are now in the production slot, which is not recommended for setting up CI/CD" reinforces the slot-first guidance: use Deployment Center to scaffold against a staging slot, then promote with azure/webapps-deploy@v3 and a swap step as in the complete example. Use this blade for scaffolding and inspection only - the production workflow should live in .github/workflows/ under version control and pin the azure/login and azure/webapps-deploy major versions explicitly.

Workflow Template

name: deploy-app-service

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AZURE_WEBAPP_NAME: my-app-service
  PACKAGE_PATH: ./dist

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build application
        run: |
          npm ci
          npm run build
          npm test

      - name: Sign in to Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to production app
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          package: ${{ env.PACKAGE_PATH }}
Command/Parameter Purpose
name: deploy-app-service Gives the workflow a readable name in GitHub Actions.
on: Declares the events that trigger the workflow.
push: Runs the workflow when code is pushed.
branches: Starts the branch filter list for the push trigger.
- main Limits the push trigger to the main branch.
permissions: Defines the GitHub token permissions available to the job.
id-token: write Allows the workflow to request an OIDC token for Azure authentication.
contents: read Allows the job to read repository contents.
env: Defines reusable environment variables for later steps.
AZURE_WEBAPP_NAME: my-app-service Stores the target App Service name.
PACKAGE_PATH: ./dist Stores the path to the built deployment artifact or folder.
jobs: Starts the workflow job definitions.
build-and-deploy: Names the job that builds and deploys the application.
runs-on: ubuntu-latest Chooses the GitHub-hosted runner image.
steps: Lists the ordered actions the runner executes.
name: Checkout repository Labels the source checkout step in the Actions log.
uses: actions/checkout@v4 Checks out the repository onto the runner.
name: Build application Labels the build and test step in the Actions log.
run: | Starts a multiline shell script step.
npm ci Installs dependencies from the lockfile for a reproducible build.
npm run build Builds the application before deployment.
npm test Runs automated tests before release.
name: Sign in to Azure Labels the Azure authentication step in the Actions log.
uses: azure/login@v2 Signs in to Azure from GitHub Actions.
with: Supplies input parameters to the Azure login action.
client-id: ${{ secrets.AZURE_CLIENT_ID }} Passes the Microsoft Entra application or managed identity client ID from the AZURE_CLIENT_ID repository secret.
tenant-id: ${{ secrets.AZURE_TENANT_ID }} Passes the Azure tenant ID from the AZURE_TENANT_ID repository secret.
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Passes the Azure subscription ID from the AZURE_SUBSCRIPTION_ID repository secret.
name: Deploy to production app Labels the production deployment step in the Actions log.
uses: azure/webapps-deploy@v3 Deploys the package or folder to Azure App Service.
with: Supplies input parameters to the App Service deployment action.
app-name: ${{ env.AZURE_WEBAPP_NAME }} Identifies which App Service app receives the deployment by reusing the workflow environment variable.
package: ${{ env.PACKAGE_PATH }} Points the deploy action to the prepared build output by reusing the workflow environment variable.

Deploy to a Staging Slot

      - name: Deploy to staging slot
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          slot-name: staging
          package: ${{ env.PACKAGE_PATH }}
Command/Parameter Purpose
name: Deploy to staging slot Labels the deployment step in the workflow log.
uses: azure/webapps-deploy@v3 Uses the App Service deployment action.
with: Supplies inputs to the deployment action.
app-name: ${{ env.AZURE_WEBAPP_NAME }} Selects the target App Service app by reusing the workflow environment variable.
slot-name: staging Directs the deployment to the staging slot.
package: ${{ env.PACKAGE_PATH }} Reuses the build output path from the workflow environment variable.

Complete Example with Slot Deployment and Swap

name: build-validate-promote

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AZURE_WEBAPP_NAME: my-app-service
  RESOURCE_GROUP: rg-app-service-prod
  PACKAGE_PATH: ./dist
  SLOT_NAME: staging

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install, build, and test
        run: |
          npm ci
          npm run build
          npm test

      - name: Sign in to Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy package to staging slot
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          slot-name: ${{ env.SLOT_NAME }}
          package: ${{ env.PACKAGE_PATH }}

      - name: Smoke test staging slot
        run: |
          curl --silent --show-error --fail "https://${{ env.AZURE_WEBAPP_NAME }}-${{ env.SLOT_NAME }}.azurewebsites.net/health"

      - name: Swap staging into production
        run: |
          az webapp deployment slot swap \
            --resource-group $RESOURCE_GROUP \
            --name $AZURE_WEBAPP_NAME \
            --slot $SLOT_NAME \
            --target-slot production \
            --output json
Command/Parameter Purpose
name: build-validate-promote Gives the end-to-end release workflow a readable name.
on: Declares the events that start the workflow.
push: Triggers the workflow on pushes.
branches: Starts the branch filter list for the push trigger.
- main Restricts the trigger to the main branch.
permissions: Defines GitHub token permissions for the workflow.
id-token: write Allows OIDC authentication to Azure.
contents: read Allows repository checkout.
env: Centralizes values reused across multiple steps.
AZURE_WEBAPP_NAME: my-app-service Stores the production app name.
RESOURCE_GROUP: rg-app-service-prod Stores the resource group name used by Azure CLI commands.
PACKAGE_PATH: ./dist Stores the path to the built artifact.
SLOT_NAME: staging Stores the deployment slot name used during promotion.
jobs: Starts the job definitions.
deploy: Names the job that performs deployment and promotion.
runs-on: ubuntu-latest Chooses the Linux runner image.
steps: Lists the ordered workflow steps.
name: Checkout repository Labels the checkout step in the workflow log.
uses: actions/checkout@v4 Downloads the repository contents.
name: Set up Node.js Labels the Node.js setup step in the workflow log.
uses: actions/setup-node@v4 Installs the requested Node.js runtime on the runner.
with: Supplies input parameters to the Node.js setup action.
node-version: '20' Pins the Node.js major version for the build.
name: Install, build, and test Labels the build validation step in the workflow log.
run: | Starts a multiline shell script step.
npm ci Installs dependencies from the lockfile.
npm run build Builds the application.
npm test Runs the test suite before deployment.
name: Sign in to Azure Labels the Azure authentication step in the workflow log.
uses: azure/login@v2 Authenticates the runner to Azure.
with: Supplies input parameters to the Azure login action.
client-id: ${{ secrets.AZURE_CLIENT_ID }} Passes the client ID for OIDC login from the AZURE_CLIENT_ID repository secret.
tenant-id: ${{ secrets.AZURE_TENANT_ID }} Passes the tenant ID for OIDC login from the AZURE_TENANT_ID repository secret.
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Passes the subscription ID for OIDC login from the AZURE_SUBSCRIPTION_ID repository secret.
name: Deploy package to staging slot Labels the staged deployment step in the workflow log.
uses: azure/webapps-deploy@v3 Deploys the package to App Service.
with: Supplies input parameters to the App Service deployment action.
app-name: ${{ env.AZURE_WEBAPP_NAME }} Identifies the destination App Service app by reusing the workflow environment variable.
slot-name: ${{ env.SLOT_NAME }} Identifies the destination slot for the staged deployment by reusing the workflow environment variable.
package: ${{ env.PACKAGE_PATH }} Points the deploy action to the build artifact by reusing the workflow environment variable.
name: Smoke test staging slot Labels the post-deployment health check step in the workflow log.
run: | Starts the multiline shell command that verifies the staging slot health endpoint.
curl --silent --show-error --fail "https://${{ env.AZURE_WEBAPP_NAME }}-${{ env.SLOT_NAME }}.azurewebsites.net/health" Calls the staging slot health endpoint after deployment.
name: Swap staging into production Labels the production promotion step in the workflow log.
run: | Starts the multiline shell command that swaps the staging slot into production.
az webapp deployment slot swap Promotes the staged release into production.
--resource-group $RESOURCE_GROUP Selects the resource group that contains the app.
--name $AZURE_WEBAPP_NAME Selects the App Service app to swap.
--slot $SLOT_NAME Chooses the source slot that holds the candidate release.
--target-slot production Chooses production as the destination slot.
--output json Returns structured CLI output for logging or troubleshooting.

Manual approval option

For stricter release control, split deployment and swap into separate jobs and protect the production environment with GitHub environment approvals.

Advanced Topics

Secret and Credential Guidance

  • Limit the Azure role assignment scope to the specific App Service resource when possible.
  • Keep build secrets separate from deployment identity secrets.
  • Avoid mixing publish-profile deployments and OIDC-based deployments in the same workflow unless migration is intentional.

Build Strategy Guidance

  • Build the application in GitHub Actions, then deploy the built output.
  • For compiled stacks such as TypeScript, Java, or .NET, avoid deploying raw source unless App Service build automation is explicitly required.
  • Pair Actions with slots for rollback-friendly production releases.

See Also

Sources