7 min read

Refactoring GitHub Actions Workflow for Azure Static Web Apps

Justin Yoo

A while ago, I wrote a blog post about Azure DevOps Pipelines refactoring technics. GitHub Actions is also suitable for building CI/CD pipelines. But, compared to Azure DevOps, there are many spaces to achieve the same efficiency level as Azure DevOps. GitHub Actions has recently released a new feature called "Reusable Workflows", which you can reduce the refactoring concerns. Throughout this post, I'm going to refactor the existing Azure Static Web Apps CI/CD pipeline workflows, using the "reusable workflows" feature of GitHub Actions.

Workflow for Azure Static Web Apps

While provisioning an Azure Static Web Apps (ASWA) instance, GitHub Actions workflow is automatically generated by default. Here's a sample workflow. I'm pretty sure it's not that different from yours.

### original workflow: azure-static-web-apps-xxxx-xxxx-xxxx.yml ###
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
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: "webapp" # App source code path
api_location: "apiapp" # Api source code path - optional
output_location: "wwwroot" # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
action: "close"

The problem is that once you provision the ASWA instance, you can't change the name of the auto-generated workflow file. Therefore, you are only allowed to modify the file.

Let's imagine a situation. You've got a codebase that deploys to multiple Azure Static Web Apps instances – DEV, TEST and PROD, which is pretty common. In that case, you will have as many GitHub Actions workflow files as the number of ASWA instances. But all the workflow files are virtually the same as each other except their filenames. So if you refactor those workflows, the overall process would be more simplified.

For the refactoring practice, you would use the workflow_dispatch event, together with the webhook event, to call the refactored workflow. Once it's set up, unless the access token gets invalidated, you'll be able to use the workflow. But what if the access token is expired or compromised? You MUST reissue the token, which is less ideal. How can we work out this situation?

Reusable Workflows (or Called Workflow)

The reusable workflows use the newly introduced event called workflow_call. It reuses the workflows at the job level. Let's refactor the workflow above. Copy both build_and_deploy_job and close_pull_request_job jobs and paste both into a new YAML file, which is called either "reusable workflow" or "called workflow".

### reusable workflow: build-aswa.yaml ###
name: Azure Static Web Apps
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
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: "webapp" # App source code path
api_location: "apiapp" # Api source code path - optional
output_location: "dist" # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
action: "close"

According to the workflow above, there are input variables and secret variables and other variables:

  • Event Object

    • Event Name: github.event_name
    • Event Action: github.event.action
  • Secrets

    • Azure Static Web Apps API Token: secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX
    • GitHub Token: secrets.GITHUB_TOKEN
  • App Locations

    • Web App: app_location
    • API App: api_location
    • Web App Artifact: output_location

The "called" workflow can't directly access those values defined in the "caller" workflow. Therefore, they MUST be propagated from the "caller" workflow to the "called" workflow. Let's update the variable parts in the "called" workflow like below (line #7-8, 20-21, 23-24, 30-31, 33-34, 36-37, 43-44, 54-55).

### reusable workflow: build-aswa.yaml ###
name: Azure Static Web Apps
jobs:
build_and_deploy_job:
# if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
if: inputs.event_name == 'push' || (inputs.event_name == 'pull_request' && inputs.event_action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
# azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
azure_static_web_apps_api_token: ${{ secrets.aswa_token }}
# repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
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: "webapp" # App source code path
app_location: ${{ inputs.app_location }} # App source code path
# api_location: "apiapp" # Api source code path - optional
api_location: ${{ inputs.api_location }} # Api source code path - optional
# output_location: "dist" # Built app content directory - optional
output_location: ${{ inputs.output_location }} # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
# if: github.event_name == 'pull_request' && github.event.action == 'closed'
if: inputs.event_name == 'pull_request' && inputs.event_action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
# azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}
azure_static_web_apps_api_token: ${{ secrets.aswa_token }}
action: "close"

All values have become parameterised. Now, you need to define them in the "called" workflow, under the workflow_call event. All non-secret variables go under the inputs attribute (line #6-21), and all the secret variables go under the secrets attribute (line #23-27).

### reusable workflow: build-aswa.yaml ###
name: Azure Static Web Apps
on:
workflow_call:
inputs:
event_name:
required: true
type: string
event_action:
required: true
type: string
app_location:
required: true
type: string
api_location:
required: true
type: string
output_location:
required: true
type: string
secrets:
github_token:
required: true
aswa_token:
required: true
jobs:
...

Now, the refactoring has been completed!

Caller Workflow

Let's update the existing ASWA workflow. As both jobs defined under the jobs node are no longer necessary, delete them. And define a new "reusable" workflow like below:

  • Called Workflow (line #14-15): <org_name>/<repo_name>/.github/workflows/<reusable_workflow_filename>@<branch_or_tag>
  • Input Variables (line #17-22): Under with
  • Secret Variables (line #24-26): Under secrets
### caller workflow: azure-static-web-apps-xxxx-xxxx-xxxx.yml ###
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
call_aswa_flow:
uses: <org_name>/<repo_name>/.github/workflows/build-aswa.yaml@main
with:
event_name: ${{ github.event_name }}
event_action: ${{ github.event.action }}
app_location: "webapp"
api_location: "apiapp"
output_location: "dist"
secrets:
github_token: ${{ secrets.GITHUB_TOKEN }}
aswa_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_XXXX_XXXX_XXXX }}

Now, regardless of the number of "caller" workflows, update them like above. You only need to update the "called" workflow then all ASWA pipelines will get the updated workflow applied, which is really convenient!


So far, I've shown how to refactor the Azure Static Web Apps workflow with the reusable workflows feature of GitHub Actions. I'm sure that it's not just for ASWA but also can be utilised in many different scenarios.