Skip to main content

Azure Static Web Apps

1.) Setting Up Azure Tenant Resources​

Creating the Static Web App​

â„šī¸ NOTE: A GitHub admin with access to the FortisureIT GitHub account will need to temporarily disable Github IP protection during the creation of the static web app. This allows the resource to view the repository and create the link between the two.

  1. Within Github:

    • Enterprise Settings -> Authentication Security -> IP allow list -> Enable 'All IPs'
  2. Navigate to the Azure tenant that you would like to create the static web app within. This may be the FortisureIT internal tenant, or an external tenant for a client depending on the use case.

  3. Click Create a Resource at the top of the Azure tenant home page.

  4. Search for Static Web App within the marketplace, and click create.

  5. Within the Basics tab, fill out the project details, making sure to select the following under Deployment Details

    • For Source use Github
    • For Github Account use an account with access to the repository you plan to host on the static web app
    • For Organization select Fortisure-IT
    • For Repository select the relevant repository for your project
    • For Branch select main/master
    • If the repository/branch are not loading try refreshing the page. This is the step that requires the disabling of the Github IP protections.
  6. Within the Deployment Configuration tab select the following

    • For Deployment Authorization Policy select Github
  7. Add the relevant tags to the resource and create.

âš ī¸ NOTE: Remember to re-enable GitHub IP protection once the creation of the static web app resource is complete. 8. Within Github:

  • Enterprise Settings -> Authentication Security -> IP allow list -> Disable 'All IPs'

2.) Setting Up Azure VM Resources​

Repo-Level Runner​

With each new pipeline a new repo-specific runner should be added to the virtual machine to handle concurrent build actions for that repository.

â„šī¸ NOTE: This section may need re-visited in the future to balance VM resource load vs storage load as additional runners take storage space.

NOTE: You must have, or rely on someone who has, admin access to the 'GitHub-Actions' Virtual Machine for this step.

  1. Log into the 'GitHub-Actions' virtual machine using the credentials for "GitHub Actions VM (Azure)" stored in Keeper.
    • Instructions on how to log in are stored in the 'Notes' section of the credential in Keeper.
  2. Navigate to the repo-runners folder using cd ./repo-runners.
    • Alternatively, the default runner is stored in 'root/actions-runner' if you need to access it.
  3. Create a new directory for your repository's runner using mkdir ./[repo name]-runner.
  4. Navigate into your new folder with cd ./[repo name]-runner.
  5. In a new browser instance, navigate to your GitHub repo and create a new runner via repo -> settings -> actions -> runners -> new-self-hosted-runner.
  6. Run the commands GitHub gives you in the terminal.
    • Skip the first step $ mkdir actions-runner; cd actions-runner since you've already created and navigated to a custom directory.
    • When prompted for a tag make sure to add repo-runner so that workflow files using [self-hosted, repo-runner] call this runner instead of the org-level runner.
    • Skip the last step runs-on: self-hosted since we've already edited the workflow files.
  7. Once the runner is added, install GitHub Actions runner as a system service using sudo ./svc.sh install.
  8. Run the service using sudo ./svc.sh start
    • The runner service will now start on boot and allow for background execution or concurrent actions.
    • You can check the status of the runner with sudo ./svc.sh status
  9. Copy the wipe_work script into our new repo runner directory by navigating into any other repo runner directory (such as fit-docs-runner for example) and executing cp ./wipe_work.sh ~/repo-runners/[repo name]-runner.
    • This script will clean up any files left over after building
  10. Edit the script by navigating into your new repo runner directory, opening the file using nano or vim, and updating the TARGET_DIR to match your new directory.
    • TARGET_DIR = "/home/GitHubAdmin/repo-runners/[repo name]-runner/_work"
    • Make sure to save the file before exiting
  11. Grant permissions to the file by running the commands sudo chown root:root ./wipe_work.sh and sudo chmod 755 ./wipe_work.sh
    • These will require the GithubAdmin password
  12. Run sudo visudo to edit the sudoers.tmp file, adding a new line for the new wipe_work.sh file in your directory. You can follow the format of the existing additions.
    • This will also require the GithubAdmin password
    • ALL ALL=(root) NOPASSWD: /home/GitHubAdmin/repo-runners/[repo name]-runner/wipe_work.sh
    • Make sure to save the file before exiting

3.) Updating the Workflow File​

Navigate to the source GitHub repository. Note that there is now a GitHub workflow file under .github/workflows.

Workflows​

Creating the static web app in section 1 will automatically generate a default workflow file in the specified GitHub repo. Now, we need to update it to build on our internal Azure virtual machine, run our own build steps, and clean up after itself.

  1. Update the build process from running on GitHub to running on our self-hosted virtual machine.

    • Under each runs-on: line, replace ubuntu-latest with [self-hosted, repo-runner].
    • This applies to every job in the file (build_and_deploy, cleanup_runner, and close_pull_request).
  2. Add explicit build steps to the build_and_deploy job before the Azure deploy step.

    • Set up Node.js with actions/setup-node@v4, matching the version your project uses.
    • Add npm ci and npm run build for both the root project and any nested project (such as api).
    • On the Azure/static-web-apps-deploy@v1 step, set skip_app_build: true and skip_api_build: true so Azure does not try to rebuild.
  3. Update the Azure/static-web-apps-deploy@v1 step's locations to match your project layout.

    • Set app_location to your built frontend folder (e.g. 'dist').
    • Set api_location to your API folder if applicable, otherwise leave as ''.
      • Note: managed API functions are not supported on the free Static Web App tier — leave empty until upgraded.
    • Set output_location to '' since pre-built artifacts are being provided.
  4. Add OIDC token retrieval for federated GitHub authentication in both build_and_deploy and close_pull_request.

    • Install the OIDC client with npm install @actions/core@1.6.0 @actions/http-client.
    • Use actions/github-script@v6 to call coredemo.getIDToken() and capture the result.
    • Pass the token to the deploy step via github_id_token: ${{ steps.idtoken.outputs.result }}.
  5. Add a cleanup_runner job that runs the wipe_work.sh script set up in section 2.

    • Use needs: build_and_deploy and if: always() so it runs after every build, including failures.
    • The single step calls sudo /home/GitHubAdmin/repo-runners/[repo name]-runner/wipe_work.sh.

â„šī¸ NOTE: The exact name for secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_[GENERATED NAME] can be found within your repository settings on Github under Secrets and Variables -> Actions

â„šī¸ NOTE: Below is an example version of the edited workflow file.

Example Workflow File​

.github/workflows/azure-static-web-apps-[generated-name].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:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: [self-hosted, repo-runner]
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
with:
submodules: true
lfs: false
clean: true

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

- name: Install root dependencies
run: npm ci

- name: Install OIDC Client from Core Package
run: npm install @actions/core@1.6.0 @actions/http-client

- name: Get Id Token
uses: actions/github-script@v6
id: idtoken
with:
script: |
const coredemo = require('@actions/core')
return await coredemo.getIDToken()
result-encoding: string

- name: Build frontend
run: npm run build

- name: Install API dependencies
run: cd api && npm ci

- name: Build API
run: cd api && npm run build

- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
id: builddeploy
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_[GENERATED NAME] }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: upload
app_location: 'dist'
api_location: '' #UPDATE TO 'api' ONCE OFF THE FREE STATIC WEB APP TIER, DOESNT SUPPORT MANAGED API FUNCTIONS
output_location: ''
github_id_token: ${{ steps.idtoken.outputs.result }}
skip_app_build: true
skip_api_build: true

- name: SHOW TESTING LINK
run: |
echo "Testing Environment Link ---> ${{ steps.builddeploy.outputs.static_web_app_url }}"

cleanup_runner:
runs-on: [self-hosted, repo-runner]
needs: build_and_deploy
if: always()
steps:
- name: Final cleanup
run: sudo /home/GitHubAdmin/repo-runners/[repo name]-runner/wipe_work.sh


close_pull_request:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: [self-hosted, repo-runner]
permissions:
id-token: write
contents: read
pull-requests: write
issues: write
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Install OIDC Client from Core Package
run: npm install @actions/core@1.6.0 @actions/http-client

- name: Get Id Token
uses: actions/github-script@v6
id: idtoken
with:
script: |
const coredemo = require('@actions/core')
return await coredemo.getIDToken()
result-encoding: string

- name: Close pull request environment
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_[GENERATED NAME] }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
github_id_token: ${{ steps.idtoken.outputs.result }}
action: close

Use & Notes​

Your CI/CD pipeline will run based on the workflow files and should automatically run when the trigger you specified takes place.

Monitoring & Debugging​

To monitor and debug your workflows, the 'actions' tab at the top of your GitHub repository can be viewed. Additionally, during and after a workflow has ran on a specific branch, the latest state of the workflow is shown after the name of the latest commit on the 'code' page of the GitHub repository for that branch.

Additional Functions on Workflows/Actions​

Workflow files are highly customizable and can be made to handle testing, add comments to PRs, create documentation, send notifications, manage tickets, and more. Further, what triggers them can be changed to many options. The guide above is designed to get a basic CI/CD pipeline in place, but adjustment and improvement is encouraged.

We currently do not have any documentation on improvements/additions to these flows.

â„šī¸ NOTE: Documentation on any useful additions is more than welcome and should most likely go in a 'standards' or 'processes' directory on the docs site rather than in the tools section. However, adding a link to that directory/documentation here once it is created would be ideal.

GitHub Secret Key Environment-Level Security (Optional)​

As mentioned above, moving GitHub secrets from repository-level (Settings -> Secrets and variables -> Actions) to environment-level (Settings -> Environments -> Environment secrets) prevents developers with 'write' access to the repository from being able to change and delete the keys used by the workflows. Allowing changing of these keys could be dangerous as a developer could delete a key that is difficult to retrieve or, more impactfully, replace a key with their own value and cause the CI/CD pipeline to push to a different identity/web app entirely. These scenarios, however, would likely have to be intentional and malicious as it is not easy to accidentally access the secrets. So, moving secrets to the environment-level is best practice, but not functionally necessary.

Currently, we are unaware of an easy way to migrate keys to the environment-level since they cannot be read in GitHub. Likely the easiest/only way to achieve this is by recreating them one-by-one on the environment-level and regathering the values from Azure, then replacing their references in the workflow files.

FAQ​

How do I check workflow statuses and debug workflows?

  • The 'actions' tab at the top of your GitHub repository can be used to see all current and past runs of workflows in your repository. This does include all branches in the repository.
    • Failed runs will be displayed and can be clicked to view more details on what exactly failed.
    • Current runs can be clicked to view a real-time log of the process.
    • Some or multiple steps of the process can be re-ran from here.

How do I restart or check the status of runners?

  • To check the status of a runner on the 'GitHub Actions' virtual machine, first log into the VM using credentials in Keeper. Then navigate to the directory of the runner you are trying to check or restart (likely either cd actions-runner for the default or cd repo-runners/[repo name]-runner for a repository-specific runner).
    • To check the runner status use sudo ./svc.sh status
    • To restart the runner use sudo ./svc.sh stop; sudo ./svc.sh start