Migrating from GitHub Pages to Azure Static Web Apps
You can use Bicep and GitHub Actions to build and deploy to a static website on Azure Static Web Apps. This post demonstrates how.

Why migrate?
This blog has been hosted on GitHub Pages for some time. It also makes use of Netlify for deployment previews. These are both great, but it's always niggled that there's two mechanisms in play; each separately configured. It's time to simplify.
Azure Static Web Apps supports both hosting static websites and deployment previews (known as "staging environments"). So we're going to migrate across to use Static Web Apps in place of both of GitHub Pages and Netlify. I'm choosing to use Bicep to do this as I tend towards using infrastructure as code. If you wanted to roll with a more "point and click" approach in the Azure Portal, you could do that too. Simply ignore the Bicep related portions of the post.
Bicep
The first thing we're going to need is a Bicep template to deploy our SWA. In our GitHub repo we're going to add a infra folder, and in there we'll create a main.bicep file:
param location string
param branch string
param name string
param tags object
@secure()
param repositoryToken string
param customDomainName string
resource staticWebApp 'Microsoft.Web/staticSites@2021-02-01' = {
  name: name
  location: location
  tags: tags
  sku: {
    name: 'Free'
    tier: 'Free'
  }
  properties: {
    repositoryUrl: 'https://github.com/johnnyreilly/blog.johnnyreilly.com'
    repositoryToken: repositoryToken
    branch: branch
    provider: 'GitHub'
    stagingEnvironmentPolicy: 'Enabled'
    allowConfigFileUpdates: true
    buildProperties:{
      skipGithubActionWorkflowGeneration: true
    }
  }
}
// resource customDomain 'Microsoft.Web/staticSites/customDomains@2021-02-01' = {
//   parent: staticWebApp
//   name: customDomainName
//   properties: {}
// }
output staticWebAppDefaultHostName string = staticWebApp.properties.defaultHostname // eg gentle-bush-0db02ce03.azurestaticapps.net
output staticWebAppId string = staticWebApp.id
output staticWebAppName string = staticWebApp.name
Most of the Bicep template above is self-explanatory. There's a few things to highlight:
- We're using the "Free" SKU which means we don't have to pay to run our website.
- We need to provide a repositoryToken- this is a little surprising as you'll see later in the template that we supply theskipGithubActionWorkflowGeneration: truewhich means we're not requiring our SWA to interact with GitHub on our behalf - but it seems that there's a requirement for a GitHub token anyway. We'll roll with it.
- We're enabling deployment previews / staging environments with stagingEnvironmentPolicy: 'Enabled'
- The branchis always set tomain- we have to let Azure know this so it knows which branch is the primary branch and hence which other ones will have staging environments.
- It also includes a section for the custom domain which is commented out - we'll uncomment that later once we've set up our custom domain / DNS.
Setting up a resource group
With our Bicep in place, we're going to need a resource group to send it to. We're going to create ourselves a resource group in West Europe:
az group create -g rg-blog-johnnyreilly-com -l westeurope
Secrets for GitHub Actions
We're aiming to set up a GitHub Action to handle our deployment which depends upon some secrets.
AZURE_CREDENTIALS - GitHub logging into Azure
First a AZURE_CREDENTIALS secret that facilitates GitHub logging into Azure. We'll use the Azure CLI to create this:
az ad sp create-for-rbac --name "myApp" --role contributor \
    --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
    --sdk-auth
Remember to replace the {subscription-id} with your subscription id and {resource-group} with the name of your resource group (rg-blog-johnnyreilly-com if you're following along). This command will pump out a lump of JSON that looks something like this:
{
  "clientId": "a-client-id",
  "clientSecret": "a-client-secret",
  "subscriptionId": "a-subscription-id",
  "tenantId": "a-tenant-id",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}
Take this and save it as the AZURE_CREDENTIALS secret in GitHub.
WORKFLOW_TOKEN - Azure accessing the GitHub container registry
We also need a secret for updating workflows from Azure. Azure Static Web Apps can update your workflow - they need access to do this when we're deploying. To facilitate this we'll set up a WORKFLOW_TOKEN secret. This is a GitHub personal access token with the workflow scope. Follow the instructions here to create the token.
Ironically, we're not planning to use this functionality, but the validation for the Bicep template will fail if it isn't supplied.
Deploying with GitHub Actions
With our secrets configured, we're now well placed to update our GitHub Action. We'll tweak the content of .github/workflows/build-and-deploy.yaml file in our repository to the following:
name: Build and Deploy
on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main
env:
  RESOURCE_GROUP: rg-blog-johnnyreilly-com
  LOCATION: westeurope
  STATICWEBAPPNAME: blog.johnnyreilly.com
  TAGS: '{"owner":"johnnyreilly", "email":"johnny_reilly@hotmail.com"}'
jobs:
  build_and_deploy_swa_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and deploy
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - name: Set Deployment Name
        id: deployment_name
        run: |
          REF_SHA='${{ github.ref }}.${{ github.sha }}'
          DEPLOYMENT_NAME="${REF_SHA////-}"
          echo "DEPLOYMENT_NAME=$DEPLOYMENT_NAME" >> $GITHUB_OUTPUT
      - name: Static Web App - change details
        id: static_web_app_what_if
        if: github.event_name == 'pull_request'
        uses: azure/CLI@v2
        with:
          inlineScript: |
            az deployment group what-if \
              --resource-group ${{ env.RESOURCE_GROUP }} \
              --name "${{ steps.deployment_name.outputs.DEPLOYMENT_NAME }}" \
              --template-file ./infra/main.bicep \
              --parameters \
                  branch='main' \
                  location='${{ env.LOCATION }}' \
                  name='${{ env.STATICWEBAPPNAME }}' \
                  tags='${{ env.TAGS }}' \
                  repositoryToken='${{ secrets.WORKFLOW_TOKEN }}' \
                  customDomainName='${{ env.STATICWEBAPPNAME }}'
      - name: Static Web App - deploy infra
        id: static_web_app_deploy
        if: github.event_name != 'pull_request'
        uses: azure/CLI@v2
        with:
          inlineScript: |
            az deployment group create \
              --resource-group ${{ env.RESOURCE_GROUP }} \
              --name "${{ steps.deployment_name.outputs.DEPLOYMENT_NAME }}" \
              --template-file ./infra/main.bicep \
              --parameters \
                  branch='main' \
                  location='${{ env.LOCATION }}' \
                  name='${{ env.STATICWEBAPPNAME }}' \
                  tags='${{ env.TAGS }}' \
                  repositoryToken='${{ secrets.WORKFLOW_TOKEN }}' \
                  customDomainName='${{ env.STATICWEBAPPNAME }}'
      - name: Static Web App - get API key for deployment
        id: static_web_app_apikey
        uses: azure/CLI@v2
        with:
          inlineScript: |
            APIKEY=$(az staticwebapp secrets list --name '${{ env.STATICWEBAPPNAME }}' | jq -r '.properties.apiKey')
            echo "APIKEY=$APIKEY" >> $GITHUB_OUTPUT
      - name: Static Web App - build and deploy
        id: static_web_app_build_and_deploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ steps.static_web_app_apikey.outputs.APIKEY }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: 'upload'
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: '/blog-website' # App source code path
          api_location: '' # Api source code path - optional
          output_location: 'build' # Built app content directory - optional
          ###### End of Repository/Build Configurations ######
      - name: Static Web App - get preview URL
        id: static_web_app_preview_url
        uses: azure/CLI@v2
        with:
          inlineScript: |
            DEFAULTHOSTNAME=$(az staticwebapp show -n '${{ env.STATICWEBAPPNAME }}' | jq -r '.defaultHostname')
            echo $DEFAULTHOSTNAME
            PREVIEW_URL="https://${DEFAULTHOSTNAME/.[1-9]./-${{github.event.pull_request.number }}.${{ env.LOCATION }}.1.}"
            echo $PREVIEW_URL
            echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_OUTPUT
    outputs:
      preview-url: ${{steps.static_web_app_preview_url.outputs.PREVIEW_URL}}
  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Cleanup Pull Request staging environment
    steps:
      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - name: Get API key for deployment
        id: apikey
        uses: azure/CLI@v2
        with:
          inlineScript: |
            APIKEY=$(az staticwebapp secrets list --name '${{ env.STATICWEBAPPNAME }}' | jq -r '.properties.apiKey')
            echo "APIKEY=$APIKEY" >> $GITHUB_OUTPUT
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ steps.apikey.outputs.APIKEY }}
          action: 'close'
The above workflow does the following:
- For main branch deployments it releases our static web app making use of Bicep. For pull requests it tells us if there's any changes that the current PR would make to our SWA as a consequence.
- It acquires an API Key from Azure which can then be used to perform a deployment.
- It deploys using the dedicated GitHub Action for SWAs
- It calculates the preview URL for a given pull request (it isn't used as yet, but could be)
- When a pull request is closed it triggers the GitHub Action to clean up the preview environment.
DNS and custom domains
Once our GitHub Action has run for the first time on the main branch, we'll be deploying to Azure Static Web Apps.
Once we've started deploying there, we want to get our custom domain set up to point to it. To do this, we're going to fire up the Azure Portal and go to add a custom domain:

We're going to add a TXT record for my blog. Azure generates a code for us:

We need to take that code and go a register it with our DNS provider. In my case that's Cloudflare, so we can go there and add it:

After a while (I think about twenty minutes in my case), this lead to the domain name being validated:

Now that we have a custom domain set up in Azure, we want to uncomment the resource customDomain portion of the Bicep template now as well:
resource customDomain 'Microsoft.Web/staticSites/customDomains@2021-02-01' = {
  parent: staticWebApp
  name: customDomainName
  properties: {}
}
This will mean that subsequent deployments to Azure do not wipe out our newly configured domain name.
We're now ready to start pointing our DNS to the Static Web Apps instance. We jump back across to Cloudflare and we amend the CNAME record that currently points to johnnyreilly.github.io, and switch it to point to the auto-generated domain in Azure:

And just like that, we're hosted on Static Web Apps!
