Azure Static Web Apps is a new Azure service launched in Preview at Build 2020, which offers an app service dedicated to static websites. You could say "why is it so special"? In the end, hosting a static website is relatively simple, since there isn't any server-side component: no databases, no ASP.NET Core or PHP or Node runtime, just pure HTML, CSS and JavaScript. Azure Static Web Apps is more than just hosting for static content: thanks to its tight connection to GitHub, it supports creating automated pipelines (through GitHub Actions) which are able to automatically build and deploy full stack web apps.
This service is a great companion for frameworks like Gatsby or Hugo, which are able to generate static pages as part of the build process. Let's say that you're using Hugo to host your personal blog. Thanks to Azure Static Web App, you can just simply commit to your GitHub repository a markdown file with your latest post to trigger the execution of a GitHub Action, which will build it as a static page.
And if you need to add a server-side component (for example, hosting a REST API), the Azure Static Web App service supports the deployment of serverless APIs based on Azure Functions.
However, as you know, in my everyday job I'm focused on the development and the deployment of Windows desktop applications. And, as you probably know if you follow this blog or if you have read my book about MSIX, I'm a big fan of MSIX and the App Installer technology, which enables to easily have a solid CI/CD story for Windows applications. Thanks to them, in fact, we can enable features like automatic updates, critical updates, differential updates, etc. And guess what? All you need is a static website that can host your MSIX package, plus the AppInstaller file.
I guess now you know where I'm headed to =) Let's see how we can use an Azure Static Web App to host our MSIX package, which will be automatically built and deployed by a GitHub Action.
As first step, let's create our Azure Static Web App. Login to the Azure portal with your account and click on Create a resource. Start a search using the static keyword. One of the results will be Static Web App (Preview), as in the following image:
Click on it and choose Create to start the process:
The first information you have to provide are the subscription, the resource group, the name of the app and the region. The only available SKU, for the moment, is the Free one, since the service is in preview. Currently this service works only with GitHub, since it doesn't use it just to connect to the repository, but it's going to create the GitHub Action needed to compile and deploy the project for us. As such, the next required step is to click on Sign in with GitHub to complete the login process with your GitHub account. Once you are logged in, you will have the opportunity to choose an organization, a repository and a branch from the ones have on GitHub:
For our scenario we're going to choose a repository which contains a .NET Desktop application packaged with the Windows Application Packaging Project. The one I'm using for this blog post is available at https://github.com/qmatteoq/ContosoApp. It's just a plain WPF application based on .NET Core 3.1.
Once you have completed the configuration, press Review + create, followed by Create in the review page. Once the deployment has been completed, you will notice a few changes in your GitHub repository:
There will be a new folder, called github/workflows
with a YAML file inside. That's our GitHub workflow. We can see it also by clicking on the Actions tab in the repository:
The workflow is called Azure Static Web Apps CI/CD and it will be executed immediately. However, it will fail, since our repository contains a desktop application and the default workflow isn't tailored for this scenario. Don't worry, we're going to fix that!
If you go to the Settings section of the repository and you move to the Secrets tab, you will find out that Azure Static Web App has added a new secret for you:
This secret contains the API token which is required by the GitHub Action to connect to the Azure Static Web App instance we have created, in order to perform the deployment.
Let's take a look at the workflow that the Azure Static Web App has created for us:
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- master
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@v0.0.1-preview
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_DESERT_05F99C503 }}
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 you app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/" # App source code path
api_location: "api" # Api source code path - optional
app_artifact_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'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v0.0.1-preview
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_DESERT_05F99C503 }}
action: "close"
The workflow contains two distinct jobs:
As you can imagine, however, for our scenario this isn't really applicable, since we aren't really deploying a website, but a desktop application to be deployed. As such, we're going to basically delete everything from this workflow file, except for the Build and deploy task, which is based on the action called Azure/static-web-apps-deploy@v0.0.1-preview. We're going to use this one to publish the output of the Visual Studio compilation: the MSIX package, the AppInstaller file and the web page to trigger the installation.
To customize the workflow, we're going to take inspiration from the template created by .NET Team, which is a great starting point when you want to build desktop .NET Core applications on GitHub. This template perfectly fits my scenario: I have a WPF application based on .NET Core and a Windows Application Packaging Project to generate the MSIX package.
Let's start by creating a copy of the existing workflow file. We're going to need it later. Now open on GitHub the workflow file (in my case, it's https://github.com/qmatteoq/ContosoApp/blob/master/.github/workflows/azure-static-web-apps-calm-dese...) and click on the edit icon (the small pencil in the toolbar):
When you do this, GitHub will propose you a special edit interface tailored for workflows. Thanks a to a panel on the right, you'll be able to easily browse the Actions marketplace and integrate tasks in the workflow.
For the moment, let's delete everything (except for the first line which defines the name
) and replace the content with the one from the .NET Desktop workflow. This is how your workflow should look like:
name: Azure Static Web Apps CI/CD
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
matrix:
configuration: [Debug, Release]
runs-on: windows-latest # For a list of available runner types, refer to
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
env:
Solution_Name: your-solution-name # Replace with your solution name, i.e. MyWpfApp.sln.
Test_Project_Path: your-test-project-path # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj.
Wap_Project_Directory: your-wap-project-directory-name # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package.
Wap_Project_Path: your-wap-project-path # Replace with the path to your Wap project, i.e. MyWpf.App.Package\MyWpfApp.Package.wapproj.
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
# Install the .NET Core workload
- name: Install .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.101
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@2008f912f56e61277eefaac6d1888b750582aa16
# Execute all unit tests in the solution
- name: Execute unit tests
run: dotnet test
# Restore the application to populate the obj folder with RuntimeIdentifiers
- name: Restore the application
run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration
env:
Configuration: ${{ matrix.configuration }}
# Decode the base 64 encoded pfx and save the Signing_Certificate
- name: Decode the pfx
run: |
$pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.Base64_Encoded_Pfx }}")
$certificatePath = Join-Path -Path $env:Wap_Project_Directory -ChildPath GitHubActionsWorkflow.pfx
[IO.File]::WriteAllBytes("$certificatePath", $pfx_cert_byte)
# Create the app package by building and packaging the Windows Application Packaging project
- name: Create the app package
run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }}
env:
Appx_Bundle: Always
Appx_Bundle_Platforms: x86|x64
Appx_Package_Build_Mode: StoreUpload
Configuration: ${{ matrix.configuration }}
# Remove the pfx
- name: Remove the pfx
run: Remove-Item -path $env:Wap_Project_Directory\$env:Signing_Certificate
# Upload the MSIX package: https://github.com/marketplace/actions/upload-artifact
- name: Upload build artifacts
uses: actions/upload-artifact@v1
with:
name: MSIX Package
path: ${{ env.Wap_Project_Directory }}\AppPackages
Let's go and customize a few things. First, you'll need to customize the environment variables defined at the top to point to your solutions and project. You'll have to setup:
Solution_Name
with the relative path of the solution fileWap_Project_Directory
with the relative path of the folder which contains the Windows Application Packaging ProjectWap_Project_Path
with the full path of Windows Application Packaging Project fileOptionally, you can also use the Test_Project_Path
to configure the path of the project which contains your unit tests. In my case, since it's a sample project, I don't have one, so I simply removed that variable. This is how my environment configuration looks like:
env:
Solution_Name: ContosoApp.sln
Wap_Project_Directory: ContosoApp.Package
Wap_Project_Path: ContosoApp.Package\ContosoApp.Package.wapproj
Second, for our scenario, we don't really need to build the application both in Debug and Release mode. We just want to have, as output, a single release MSIX package, so that we can deploy it on our website. As such, remove from the configuration matrix the Debug
entry:
strategy:
matrix:
configuration: [Release]
As next step, I'm going to customize some of the available tasks:
Execute unit tests
isn't needed for my sample application, since I don't have unit tests. As such, I'm going to delete this one.
Build
. Since we're going to use the AppInstaller technology to distribute the package using our Azure Static Web App, we need to tweak a bit the various parameters.
Appx_Package_Build_Mode
parameter, which by default is set to StoreUpload
, which is the needed when you want to publish the application on the Microsoft Store. In this case we're using manual distribution, so we need to change it to SideloadOnly
./p:GenerateAppInstallerFile
parameter and set it to true
, in order to generate the .appinstaller file and the web page as part of the process./p:AppInstallerUri
parameter and set it to the URL that has been assigned to our Azure Static Web App. You can find this URL in the Azure portal:
This is how the task looks like after the changes:
# Create the app package by building and packaging the Windows Application Packaging project
- name: Create the app package
run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:GenerateAppInstallerFile=$env:Generate_AppInstaller /p:AppInstallerUri=$env:AppInstaller_Url /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }}
env:
Appx_Bundle: Always
Appx_Bundle_Platforms: x86|x64
Appx_Package_Build_Mode: SideloadOnly
Configuration: ${{ matrix.configuration }}
Generate_AppInstaller: true
AppInstaller_Url: https://calm-desert-05f99c503.azurestaticapps.net
As I have discussed multiple times in this blog, signing is a critical aspect of MSIX packaging. If you don't sign a MSIX package with a trusted certificate, the user will never be able to install it. At the same time, you must be very careful in how you enable signing as part of a CI/CD pipeline. If you expose your private certificate, someone might be able to steal it and use your identity to sign malicious applications. There are multiple approaches to sign MSIX packages in the right way as part of a CI/CD pipeline. The approach used by the workflow template is to ask to the developer to encode the PFX into a base64 string and store it as a secret. Then the workflow includes a PowerShell script which takes care of decoding the secret back into a PFX, so that it can be used as part of the Visual Studio build to do the signing.
The main reasoning of using this approach on GitHub is that, unlike Azure DevOps, the platform doesn't have the concept of Secure Files, which is often used in scenarios like this one.
The GitHub Action workflow already contains everything you need to sign the package. The only missing step is to create the secrets to store the various required information.
Open a PowerShell terminal in the folder in which you keep your PFX certificate.
Run the following script:
$pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte
[System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt'
At the end of the process you will find, in the same folder, a text file called SigningCertificate_Encoded.txt, which contains the certificate encoded as a base64 string. Open the text file and copy the whole content.
Now go to your repository on GitHub, choose Settings → Secrets and create a new secret.
Call the secret Base64_Encoded_Pfx
and, as value, paste the encoded base64 string you have just copied.
Now create a new secret and call it Pfx_Key
. As value, type the password for the signing certificate.
That's it. Another and safer alternative to this approach is to leverage Azure Key Vault to store your certificate and the Azure SignTool utility to do the actual signing as part of the pipeline. You can find all the details about this approach, including how to use it in a GitHub Action workflow, in a recent blog post I wrote.
We have finished the work on the build pipeline. This is how it should look like after the changes:
name: Azure Static Web Apps CI/CD
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
matrix:
configuration: [Release]
runs-on: windows-latest # For a list of available runner types, refer to
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
env:
Solution_Name: ContosoApp.sln
Wap_Project_Directory: ContosoApp.Package
Wap_Project_Path: ContosoApp.Package\ContosoApp.Package.wapproj
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@2008f912f56e61277eefaac6d1888b750582aa16
# Restore the application to populate the obj folder with RuntimeIdentifiers
- name: Restore the application
run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration
env:
Configuration: ${{ matrix.configuration }}
# Decode the base 64 encoded pfx and save the Signing_Certificate
- name: Decode the pfx
run: |
$pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.Base64_Encoded_Pfx }}")
$certificatePath = Join-Path -Path $env:Wap_Project_Directory -ChildPath GitHubActionsWorkflow.pfx
[IO.File]::WriteAllBytes("$certificatePath", $pfx_cert_byte)
# Create the app package by building and packaging the Windows Application Packaging project
- name: Create the app package
run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:GenerateAppInstallerFile=$env:Generate_AppInstaller /p:AppInstallerUri=$env:AppInstaller_Url /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }}
env:
Appx_Bundle: Always
Appx_Bundle_Platforms: x86|x64
Appx_Package_Build_Mode: SideloadOnly
Configuration: ${{ matrix.configuration }}
Generate_AppInstaller: true
AppInstaller_Url: https://calm-desert-05f99c503.azurestaticapps.net
# Remove the pfx
- name: Remove the pfx
run: Remove-Item -path $env:Wap_Project_Directory\GitHubActionsWorkflow.pfx
# Upload the MSIX package: https://github.com/marketplace/actions/upload-artifact
- name: Upload build artifacts
uses: actions/upload-artifact@v1
with:
name: MSIX Package
path: ${{ env.Wap_Project_Directory }}\AppPackages
This is enough to get a ready to be deployed MSIX package. If you commit any change to the repository which hosts your desktop app, the process should complete without errors and, in the end, you should have an artifact folder which contains:
Now it's time to go back to the original workflow file that was created when we have created the Azure Static Web App and that we have copied before starting making changes. We need, in fact, to leverage the task that will take care of deploying on the Web App the artifact we have just created. However, in order to do this, we're going to create a different job. Its purpose will be to download the artifact we have generated and to deploy it.
Why using a separate job? The first reason is more "philosophical". The deployment is a different step compared to the build process. Azure DevOps does a great job in helping to keep the two phases separated, by providing release pipelines to handle the release management story. GitHub doesn't support this approach but, still, a multi-stage pipeline can help us to better split the two phases. The second reason is more practical. The action which performs the deployment to Azure Static Web App (the Azure/static-web-apps-deploy@v0.0.1-preview
one) works only on Linux, while the build process we have executed so far must run on Windows. By using multiple jobs, we can use different environments: the build job will continue to run on a Windows hosted agent, while we're going to run the deployment job on a Linux machine.
Let's go and add the second job after the first one we have previously configured:
deploy:
needs: [build]
runs-on: ubuntu-latest
name: Deploy Job
steps:
- name: Download Package artifact
uses: actions/download-artifact@master
with:
name: MSIX Package
path: MSIX
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v0.0.1-preview
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_DESERT_05F99C503 }}
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 you app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "MSIX" # App source code path
###### End of Repository/Build Configurations ######
This job contains only two tasks:
The first one uses the action actions/download-artifact
to download on the machine the artifact we have just created in the build job. The name
property must match the value of the name
property we have set in the actions/upload-artifact
task of the build job.
The second one is the task we have previously copied from the original workflow file, which takes care to do the deployment to the Azure Static Web App. Compared to the original task, we have to make a couple of changes:
app_location
parameter must be set with the name of the folder that contains the artifact we need to deploy. In our case, it's MSIX
, which is the value we have set as path
in the actions/download-artifact
task.api_location
and app_artifact_location
properties, since we aren't deploying a full static web app.As you can notice, we have configured the job to run on Linux (runs-on: ubuntu-latest
) and we have used the needs
parameter to specify that this task must run only after the build
one has been completed. Without this option, GitHub would try to run both jobs in parallel.
That's it! Now try to commit any change to the code to trigger the execution of the workflow. If you did everything in the correct way, once the workflow is completed, you should be able to open your browser on the URL assigned to your Azure Static Web App and see the web page generated by Visual Studio to trigger the installation:
By clicking on the Get the app button, you will trigger the process to start the installation:
In this blog post we have seen how to use the recently announced Azure Static Web App service to host our MSIX packages and enable an easy installation process of our desktop apps through a website. By tweaking the project we can also enable automatic updates: we just need to configure the AppInstaller file to support automatic updates and use a tool like Nerdbank.GitVersioning to handle the versioning of the MSIX packages, so that we're sure that every new generated package will have a higher version number.
The sample project used in this blog post (including the GitHub workflow) is available here. If, instead, you want to see a more advanced approach, you can take a look at another project from me. This second repository contains a more complete workflow, which includes versioning management and signing using Azure Key Vault.
A special thanks to Rafael Rivera and Mitchell Webster for the help in troubleshooting and fixing a series of issues that were preventing Azure Static Web Apps to work properly with MSIX deployment.
Happy deployment!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.