| Comments

One of the things that I like about Azure DevOps Pipelines is the ability to make minor changes to your code/branch but not have full CI builds happening.  This is helpful when you are updating docs or README or things like that which don’t materially change the build output.  In Pipelines you have the built-in functionality to put some comments in the commit message that trigger (or don’t trigger rather) the CI build to stop.  The various ones that are supported are identified in ‘Skipping CI for individual commits’ documentation.

Today that functionality isn’t built-in to GitHub Actions, but you can add it as a base part of your workflows with the help of being able to get to the context of the commit before a workflow starts!  Here is an example of my workflow where I look for it:

name: .NET Core Build and Deploy

on:
  push:
    branches:
      - master

jobs:
  build:
    if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false
    name: Build Package 
    runs-on: ubuntu-latest

You can see at Line 10 that I’m looking at the commit message text for: ***NO_CI***, [ci skip], or [skip ci].  If any of these are present then the job there does not run.  It’s as simple as that!  Here is an example of my last commit where I just was updating the repo to include the build badge:

Screenshot of a commit message on GitHub

And you can see in the workflows that it was not run:

Screenshot of workflow status on GitHub

A helpful little tip to add to your workflows to give you that flexibility!  Hope this helps!

| Comments

I’ve continuing been doing research on GitHub Actions for .NET developers and came across a comment that someone said (paraphrasing): I wish I could use it for .NET Framework apps but it is just .NET Core.

NOT TRUE! And I want to help fix that perception.  There are some bumps in the road, but allow me to explain some simple (yes I realize they are simple) steps to get it working.

NOTE: I’ve been on this research because I’m looking to better get ‘publish’ experiences in Visual Studio for your apps, but I want to help you get into best practices for CI/CD and DevOps practices.  Basically I’m on a mission for right-click, publish to CI to improve for you :-)

So in this post I’ll walk through an ASP.NET Framework (MVC) app and have it build/publish artifacts using GitHub Actions.  Let’s get started…

The simple app

I am starting from File…New Project and selecting the ASP.NET Web Application (.NET Framework):

Screenshot of template selection

So it’s basic vanilla and I’m not changing anything.  The content of the app is not important for this post, just that we have a full .NET Framework (I chose v4.8) app to use.  From here in Visual Studio you can build the app, run, debug, etc.  Everything you need here is in Visual Studio of course.  If you wanted to use a terminal to build this app, you’d be likely (recommended) using MSBuild to build this and not the dotnet CLI.  The command might look something like this:

code

I’m specifying to build the solution and use a release profile.  We’ll come back to this, now let’s move on.

Publish profile

Now for our example, I want to publish this app using some pre-compiled options.  In the end of the publish task I’ll have a folder that I’d be able to deploy to a web server.  To make this simple, I’m using the Publish capabilities in Visual Studio to create a publish profile.  You get there from right-click Publish (don’t worry, we’re not publishing to production but just creating a folder profile).

Publish profile screenshot

The end result is that it will create a pubxml file in the Properties folder in your solution

Publish profile in solution explorer

So we have our app and our publish (to a folder) profile.  Moving on to the next step!

Publish to the repo and create initial GitHub Actions workflow

From Visual Studio we can add this to GitHub directly.  In the lower right of visual Studio you’ll see the ability to ‘Add to Source Control’ and select Git:

Add to source control tray button

which will bring up the UI to create/push a new repository to GitHub directly from Visual Studio:

Publish to GitHub from VS

Now we have our project in GitHub and we can go to our repository and create the initial workflow.

NOTE: This is the area if you have comments about please do so below.  In the workflow (pun intended) right now you leave Visual Studio and go to GitHub to create a new workflow file then have to pull/sync, etc.  You don’t *have* to do this but usually this is the typical workflow to find templates of workflow files for your app.  Got feedback on what Visual Studio might do here, share below!

Now that you have the publish profile created and your solution in GitHub you’ll need to manually add the pubxml file to the source control (as by default it is a part of the .gitignore file).  So right click that file in solution explorer and add to your source control.  Now on your repository in GitHub go to the Actions tab and setup a new workflow:

Setting up new workflow

The reason for this (in choosing new) is that you won’t see a template that is detected for .NET Framework.  And due to whatever reason GitHub thinks this is a JavaScript repository.  Anyhow, we’re effectively starting with blank.  Create the workflow and you’ll get a very blank default:

name: CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/[email protected]
    - name: Run a one-line script
      run: echo Hello, world!
    - name: Run a multi-line script
      run: |
        echo Add other actions to build,
        echo test, and deploy your project.

And it will not be helpful, so we’ll be wiping it out.  I’ve named my workflow build.yml as I’m only focusing on build right now. 

Defining the .NET Framework build steps

For this post I’m going to put all the steps here rather than build-up and explain each one so you can see the entirety.  Here’s the final script for me:

name: Build Web App

on: [push]

jobs:
  build:

    runs-on: windows-latest

    steps:
    - uses: actions/[email protected]
      name: Checkout Code
    
    - name: Setup MSBuild Path
      uses: warrenbuckley/[email protected]
      
    - name: Setup NuGet
      uses: NuGet/[email protected]
    
    - name: Restore NuGet Packages
      run: nuget restore SimpleFrameworkApp.sln

    - name: Build and Publish Web App
      run: msbuild SimpleFrameworkApp.sln /p:Configuration=Release /p:DeployOnBuild=true /p:PublishProfile=FolderProfile

    - name: Upload Artifact
      uses: actions/[email protected]
      with:
        name: published_webapp
        path: bin\Release\Publish

Let’s start explaining them.

Ensuring the right runner

In a previous post I described what a ‘runner’ is: What is a GitHub Action Runner?  In that post I pointed to the documentation of runners including what is installed on them.  Now for .NET Framework apps we need to use Windows because .NET Framework only works on Windows :-).  Our action needs to specify using Windows and we are using the windows-latest runner image as we are on Line 8.  I won’t spend time talking about self-hosted runners here, but regardless even your self-hosted runner needs to support .NET Framework.  As a part of the windows-latest runner image, you can see what is already provided on the image.  Currently windows-latest is defined as Windows Server 2019 and the documentation shows what is provided on the hardware resource.  This includes already having a version of Visual Studio 2019 installed…which means MSBuild is already there!

Setting up MSBuild

Even though Visual Studio is on the runner, MSBuild is not presently in the default PATH environment (as of the date of this writing)…so you have options.  The documentation provides the path to where Visual Studio is installed and you can determine the right location to MSBuild from there and specify the path fully.  However, I think there should be easier ways to do this and the community agrees!  In the marketplace there is an Action you can use to setup the PATH to have the MSBuild toolset in your path and you can see this being used on Line 14/15.  The action here basically does a ‘vswhere’ and sets up the ability to later just call MSBuild directly.  This only does MSBuild and not other VS tools that are added to PATH as a part of the ‘Visual Studio Command Prompt’ that most people use.  But using this one we have here, we can now build our Framework app with less path ugliness.

Building and publishing the app

With our MSBuild setup in place, we can start building.  The first thing we need to do is restore any NuGet packages.  In Line 20,21 is where we use the NuGet CLI to restore the solution’s packages that are needed.

NOTE: For some reason using msbuild –t:Restore was not working at the time of this writing that I expected to work…

Once we have the packages restored, we can proceed to build.  In Line 24 is our full command to build the solution.  We are specifying some parameters:

  • Configuration – simple, we are building release bits
  • DeployOnBuild – this helps us trigger the publish step
  • PublishProfile – this uses the publish profile we specify to execute that step and all the other options we have set in that configuration.  We just have to specify the name, not the path

After the completion of this step (we didn’t set any different output folders) we will have a bunch of files in the default publish folder (which would be bin\<config>\Publish).

Publish the artifacts

Once we have the final published bits, we can upload them as the artifact for this build pipeline.  As we see starting at Line 26 we are using another action to upload our content (binaries, files) to this completed workflow as an artifact named ‘published_webapp’ and this will be associated with this run and zipped up all these assets you can download or later use these artifacts to publish to your servers, cloud infrastructure, etc.

Summary

So if you thought you couldn’t use GitHub Actions for your .NET Framework now you know you can with some extra steps that may not have been obvious…because they aren’t.  In the end you have a final build:

Picture of a final build log

What I’ve shared here I put in a sample repro: timheuer/SimpleFrameworkApp where you can see the workflow (in .github/workflows/build.yml) and the logs.  I hope this helps, please share your experiences you’d like to see in Visual Studio to help you better for GitHub Actions.

| Comments

So what exactly is a runner and how do I know what’s in it?  When you use GitHub Actions and specify:

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

What exactly does that mean ‘ubuntu-latest’?  Well a runner is defined as ‘a virtual machine hosted by GitHub with the GitHub Actions runner application installed.’  Clear? LOL, basically it is a machine that has a target operating system (OS) as well as a set of software and/or tools you may desire for completing your job.   GitHub provides a set of these pre-configured runners that you are using when you use the runs-on label and use any one of the combination of: windows-latest, ubuntu-latest (or ubuntu-18.04 or ubuntu-16.04), macosx-latest.  As of this writing the matrix is documented here with also the specs of the virtual environment: Supported runners and hardware resources.

What is on a GitHub-hosted runner?

I personally think it is good practice to never assume the tool you want is on the environment you didn’t create and you should always acquire the SDK, tools, etc. you need.  That’s just me and possibly being overly cautious especially when a definition of a hosted runner provides the tools you need.  But it makes your workflow very explicit, perhaps portable to other runners, etc.  Again, I just think it is good practice. 

Runner log

But you may want to know what exactly you can use on a GitHub-hosted runner when you specify it.  Luckily GitHub publishes this in the documentation Software installed on GitHub-hosted runners.  For example as a .NET developer you might be interested to know that the windows-latest runner has:

  • Chocolatey
  • Powershell Core
  • Visual Studio 2019 Enterprise (as of this writing 16.4)
  • WinAppDriver
  • .NET Core SDK 3.1.100 (and others)

This would be helpful to know that you could use choco install commands to get a new tool for your desired workflow you are trying to accomplish.  What if you don’t see a tool/SDK that you think should be a part of the base image?  You can request to add/update a tool on a virtual environment on their repo!  Better yet, submit a repo if you can.

How much will it cost me to use GitHub-hosted runners?

Well, if you are a public repository, it’s free.  If you are not a public repository your account gets a certain number of minutes per month for free before billing as well.  It’s pretty generous and you can read all the details here: About billing for GitHub Actions.  In your account settings under the Billing section you can see your usage.  They don’t even bother to show your usage for public repositories because it’s free.  I have one private repo that I’ve used 7 minutes on this month.  My bill is $0 so far.  The cool thing is you can setup spending limits there as well.

Can I run my own runner?

Yes! Similar to Azure Pipelines you can create and host your own self-hosted runner.  The GitHub team did an amazing job with the steps here and it seriously couldn’t be simpler.  Details about self-hosted runners (either on your local machine, your own cloud environment, etc.) can be found in About self-hosted runners documentation.  Keep in mind that now the billing is on you and you should understand the security here as well because PRs and such may end up using these agents and the documentation talks all about this.  But if you are needing to do this, the steps are dead simple and the page in your repo pretty much makes it fool proof for most cases:

Screenshot of self-hosted runner config

It’s good to know what is on the environment you are using for your CI/CD and also cool to know you can bring your own and still use the same workflow.  I’ve experimented with both and frankly like the GitHub-hosted model the best for my projects.  They don’t have unique requirements and since they are all public repositories, no cost to me.  Best of all that I don’t have to now manage an environment!

| Comments

Continuing on my research and playing around with GitHub Actions, I was looking to migrate my Alexa.NET project off of Pipelines and in to one place for my open source project.  Pipelines still has an advantage for me right now as I prefer the approval flow that I have right now.  In this post I’ll cover how I modified my build definition to now also include producing the NuGet package, signing it with my code signing certificate, and pushing it to multiple repositories.

Quick tip: if you haven’t follow Ed Thomson before he’s doing a series on GitHub Actions for the month of December.  Check out his GitHub Actions Advent Calendar!

Pre-requisites

We need to first make sure we have the tools needed in the build step, so let’s be sure to get the .NET SDK so we can use the dotnet CLI commands.  This is the start of my build-and-deploy.yaml file and each other snippet builds on this.

name: "Build and Deploy"

on:
  push:
    branches:
      - master

jobs:
  build:
    if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false
    name: Build Package
    runs-on: ubuntu-latest

    steps:
    - uses: actions/[email protected]
    - name: Setup .NET Core SDK
      uses: actions/[email protected]
      with:
        dotnet-version: 3.1.100

Starting at line 16 I write the steps to get the .NET SDK that I want to use, in this case the .NET 3.1 (which is the long-term support version now) SDK.  Now we are all set…the tools I need are on the runner.

Building the Package

The first thing we obviously need to do is ensure we have an actual NuGet package.  I perform this step during my ‘build’ job when I know things have been successfully built and tested.  After getting the SDK we can how issue our pack command.  This assumes we’ve already run dotnet build, which I didn’t show here.

    - name: Pack
      run: dotnet pack TestLib --configuration Release -o finalpackage --no-build

    - name: Publish artifact
      uses: actions/[email protected]
      with:
        name: nupkg
        path: finalpackage

You can see in line 2 where we use the dotnet CLI to pack into a NuGet package.  Note I’m using an output argument there to put the final nupkg file in a specific location.  In line 5 I am setting up the action to upload the artifact so that I can use it later in other steps in the job.  The upload-artifact agent will use the path ‘finalpackage’ and upload it into the location ‘nupkg’ for me.  It will available for me later as you’ll see.

Signing the Package

Now I want to be a good trusted provider of a library package so I’ve chosen to sign my package using a code-signing certificate.  I got mine through DigiCert.  One of the main differences between Actions and Pipelines is that Actions only has secure storage for ‘secrets’ as strings.  Pipelines has a library where you can also have secure file storage.  To sign a NuGet package, the command requires a path to a certificate file so we have to somehow get the file available for the CLI command.  Based on all the recommendations from people also doing similar activities (needing files in their actions) it seemed to be the approach was to base64-encode the file and put that as a secret…so that’s the approach I took.  I base64-encoded the contents of my PFX and set it as a secret variable named SIGNING_CERT. 

Now the next thing I need to do is not only retrieve that string, but put that into a temporary file.  Searching as best I could on forums I didn’t see an existing script or anything that people used, so I created a new action for myself to use (and you can to) called timheuer/base64-to-file.  This action takes your encoded string, decodes it to a temporary file and sets the path to that temporary file as an output for the action.  Simple enough.  Now with the pieces in place we can set up the steps:

  deploy:
    needs: build
    name: Deploy Packages
    runs-on: windows-latest # using windows agent due to nuget can't sign on linux yet
    steps:
      - name: Download Package artifact
        uses: actions/[email protected]
        with:
          name: nupkg
      
      - name: Setup NuGet
        uses: NuGet/[email protected]
        with:
          nuget-api-key: ${{ secrets.NUGET_API_KEY }}
          nuget-version: latest

      - name: Setup .NET Core SDK
        uses: actions/[email protected]
        with:
          dotnet-version: 3.1.100

      - name: Get certificate
        id: cert_file
        uses: timheuer/[email protected]
        with:
          fileName: 'certfile.pfx'
          encodedString: ${{ secrets.SIGNING_CERT }}
      
      # Sign the package
      - name: Sign NuGet Package
        run: nuget sign nupkg\*.nupkg -CertificatePath ${{ steps.cert_file.outputs.filePath }} -CertificatePassword ${{ secrets.CERT_PWD }}  -Timestamper http://timestamp.digicert.com –NonInteractive

The above is my ‘deploy’ job that does the tasks.  On line 6 is where we are retrieving the nupkg file from the artifact drop in the previous job.  After that I’m using the new nuget/setup-nuget action to acquire the NuGet CLI tools for subsequent actions.  At present, you cannot use dotnet CLI to sign a NuGet package so we have to use the NuGet tools directly.  We’ll need this later as well so it’s good we have it now.  On line 22 starts the process mentioned above to use my new action to retrieve the encoded string and put it as a temp file.  One line 31 we execute the NuGet sign CLI command to sign the package.  I have a few arguments here but pay attention to the steps.cert_file.outputs.filePath one.  That is the OUTPUT from the base64-to-file action.  The format of steps.{ID}.outputs.{VARIABLE} is what you see here…and you can see in that step I gave it an id of ‘cert_file’ to easily pull out the variable later.

Now, you may have noticed that this agent job runs on windows-latest as the OS and not ubuntu.  This is because presently package signing for NuGet can only be done on Windows machines.  Now that we have a signed package (in the same location, we just signed it and didn’t move it) we can deploy it to package registries.

Publishing the Package to NuGet

Of course for a public library I want this to be available on NuGet so I’m going to publish it there.  NuGet uses an API key authentication scheme which is supported in the dotnet CLI so we can use dotnet CLI push to publish:

      - name: Push to NuGet
        run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org

Could I have used the NuGet CLI?  Sure, but I already was using this pattern previously so I’m sticking with this from a previous Pipeline definition.  Choice is yours now that we have both CLI tools on the runner machine.  Done, now on to another registry.

Publishing the Package to GitHub Package Registry

Publishing to the new GitHub Packages Registry takes one extra step.  Since this is not the default location for NuGet, we have to instruct NuGet to let it know where to publish this package.  In your repository you will be provided with a URL from the Packages tab of your repo:

Screenshot of GitHub Packages tab

This is the publishing endpoint for the NuGet CLI.  In our Action we will need two steps: set up the source and publish to it:

      - name: Add GPR Source
        run: nuget sources Add -Name "GPR" -Source ${{ secrets. GPR_URI }} -UserName ${{ GPR_USERNAME }} -Password ${{ secrets.GITHUB_TOKEN }}

      - name: Push to GitHub Packages
        run: nuget push nupkg\*.nupkg -Source "GPR"

In line 2 is where we set up the source we are going to later use.  We can give it any name you want here.  I made the other variables Secrets for my config.  This also requires you to use the UserName/Password scheme as GitHub Packages doesn’t support NuGet API keys right now.  Another reason we need to use the NuGet CLI here.  The password you can use is provided as a default token in any GitHub Action called secrets.GITHUB_TOKEN and your repo’s actions have access to it.  In line 5 then we see us using that source and pushing our package to the GitHub Packages Registry.

Summary

So there you have it!  A GitHub Actions flow packages, signs, and publishes to two package repositories.  It would be nice to standardize on one tooling CLI and I know the teams are looking for feedback here, but it is good to know that you have 2 official supported GitHub Actions in setup-dotnet and setup-nuget to use to get the tools you need.  I hope this helps someone!

| Comments

I’ve been spending a lot of time looking at the GitHub Actions experience for .NET developers.  Right now I’m still using Azure Pipelines for my project, Alexa.NET, in building, testing, and deploying to NuGet.  As the tools and process for using DevOps tools for CI/CD have so vastly improved over the years, I’ve become a huge advocate for this being the means for your build/deploy steps…YES, even as a single developer or a smaller team.  It simply really helps you get to a purer sense of preserving the ability for your code to live on, for others to accurately build it, and for you to have peace of mind that your code works as intended.  I’m just such a huge fan now.  That said, I still think there is a place for ‘right-click publish’ activities in inner-loop development.  In fact, I use it regularly for a few internal apps I’ve written.  For simple solutions that method works well, but I certainly don’t think I can right-click-publish a full solution to a Kubernetes environment though.  I’m currently researching new tooling ways to help those ‘publish to CI/CD’ from Visual Studio (would love your opinions here) so I’ve been spending a lot more time in GitHub Actions.  I decided to look at publishing a Blazor app to Azure Storage as a static site…here’s what I did.

Setting up the Storage endpoint

The first thing you need is an Azure Storage account.  Don’t have an Azure account, no worries you can get a Free Azure account easily which includes up to 5GB of Azure Blob Storage free for the first 12 months.  Worried about pricing afterwards?  Well check out the storage pricing calculator and I’m sure you’ll see that even at 1TB storage it is cost-effective means of storage.  But any rate, you need a storage account and here are the configuration you need.

First, you may already have one.  As a developer do you create your infrastructure resources or are these provisioned for you by infra/devops roles in your company (leave comments)?  Earlier this year at Build we enabled static website hosting in Azure Storage.  You first create a Storage resource (ensuring you choose v2 which is the default, but that is the version that enables this feature).  After you create your resource scroll on the left and you’ll see ‘Static website' section.  Here’s what the configuration looks like and let me explain a few areas here:

Screenshot of the Static website configuration
All of this configuration is under the Static website area.  First you obviously need to enable it…that’s just toggling the enabled/disabled capability.  Enabling this now gets you two things: 2 endpoints (basically the URI to the website) and a specific blob contianer named $web where your static content needs to live.  The endpoints default map to this blob container without having to add a container name to the root URI.  Remember the resource group you’ve given to your storage instance here, you will need that later.

NOTE: You can later add CDN/custom domain to these endpoints, but I’m not covering those here.

The second thing you need is to set a default document and error page.  The default document for your SPA is your root entry point, and for most frameworks I’ve seen this is indeed index.html.  For Blazor WebAssembly (WASM) apps, this is also the default if you are using the template.  So you set the default document as ‘index.html’ and move on.  The error document path is another interesting one…you need to set this for SPA apps because right now the static website capability of Azure Storage does not account for custom routing rules.  What this means is that storage will throw an HTTP 404 error message when you go to something like /somepage where it actually doesn’t exist but your SPA framework knows how to handle it.  Until custom routing works on Azure Storage your error document becomes your route entry point.  So for this set the error document path to also be index.html for Blazor WASM.

NOTE: Yes this isn’t ideal for routing.  On top of that it still does show an HTTP 404 actual network message even though your route is being handled.  Azure Storage team has heard this request…working on advocating for y’all.

That’s it.  Now you have a storage endpoint with a blob container that you can begin putting your content in and browse to using your endpoint URI provided from the portal.  For a simple tool to navigate your storage, I’ve been using Azure Storage Explorer and it is intuitive to me and works well to quickly navigate your storage account and containers (and supports multi-account!). 

Setting up your Azure Service Principal credentials

The next thing you will need is a service principal credential.  This will be used to authenticate with your Azure account to be able to use DevOps tools to work on your behalf in your account.  It’s a simple process if you have a standard account.  I say this only because I know there might be some configurations for environments where you yourself don’t have access to create service principals and may need someone to create one on your behalf, or also there might be credentials you can already use.  Either way here is the process I used.

I used the Azure CLI so if you don’t have that installed go ahead and grab that and install it.  This should now be in your PATH environment and using your favorite terminal you should be able to start executing commands.  To start out, login to the CLI using `az login` – this will launch a browser for you to authenticate via your account and then issue the token in your environment so that for the remainder of your session you’ll be authenticated.  After logging in successfully running `az account show` will emit what subscription you are using and you’ll need the subscription ID later so grab that and put it somewhere on your scratch notepad for later command usage.

NOTE: If you have more than one subscription and have not set a default subscription you should set that using the `az account set` command.

Now you can use the CLI to create a new service principal.  To do that issue this command:

az ad sp create-for-rbac --name "myApp" --role contributor \
                            --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
                            --sdk-auth

Note on line 2 here that you need to replace {subscription-id} with your own actual subscription id (the GUID) and {resource-group} with the resource group name where your storage account is located.  On line 1 the “myApp” can be anything but I recommend making it meaningful as this is basically the account name of the principal.  The output of this command is your service principal.  The full JSON output.  Save this off in a place for now as we’ll need that later to configure GitHub Actions properly.  Great now to move on to the app!

Create your Blazor WASM app

I assume since you may be reading this far you aren’t new to Blazor and probably already have the tools.  As of this writing, Blazor WASM is still in preview so you have to install the templates separately to acquire them to show up in `dotnet new` and in Visual Studio File…New Project.  To do that from a terminal run:

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2

Then you will be able to create a new project.  I’m showing Visual Studio here and this is the WASM template:

Screenshot of Blazor new project dialog

In the dialog here you will see Blazor WebAssembly App and that’s what you will use.  Now you have a choice to have it ASP.NET Core hosted using that checkbox, which if you were going to do other things in ASP.NET maybe that’s what you want.  For the purposes of this article we are talking about just having the WASM app and having a place to host it that isn’t a web server with other content…just hosting static content and using storage to do so…so we aren’t checking that box.  The result will be a Blazor WASM app with no host.  Now let’s add that to GitHub.  If you are using Visual Studio 16.4+ you’ll be able to take advantage of an improved flow for pushing to GitHub.  Once you have your project, in the lower right click ‘Add to Source Control’ choosing Git and then you’ll see the panel to choose GitHub and create/push a repo right away.  You don’t have to go to GitHub site first and clone later…all in one step:

Animation of Visual Studio GitHub flow

Great!  Now we have our WASM project and we’ve created and pushed the current bits to a new GitHub repository.  Now to create the workflow.

Setup the GitHub Action Workflow

Now we’ve got an Azure Storage blob container, a service principal, a Blazor WASM project, and a GitHub repository…all set to configure the CI/CD flow now.  First let’s put that service principal as a secret in our repository.  In the settings of your repository navigate to the Secrets section and add a secret named AZURE_CREDENTIALS.  The content of this is the full content of your service principal (the JSON blob) that we generated earlier:

Screenshot of GitHub Secrets configuration

This saves the secret for us to use in the workflow and reference as a variable.  You can add more secrets here if you’d like if you wanted to add your resource storage account name as well (probably a good idea).  Secrets are isolated to the original repository so no forks get the secrets at all.  Now that we have these let’s create the workflow file.

Today, Visual Studio isn’t too helpful in authoring the YAML files (would love your feedback here too!) but a GitHub Action is just a YAML file in a specific location in your repository: .github/workflows/azure-storage-deploy.yaml.  The file name can be anything but putting it in this folder structure is what is required.  You can start in the GitHub repo itself using the Actions tab and through the online editor get some level of completion assistance to help you navigate the YAML editing.  Go to the Actions tab in your repository and create a new workflow.  You’ll be offered a starter workflow based on what GitHub thinks your project is like.  As of this writing it thinks Blazor apps are Jekyll workflows so you’ll need to expand and either find the .NET Core one or just start from a blank workflow yourself.

Screenshot of GitHub Actions config

Four my workflow I want to build, publish and deploy my app.  I’ve separated it into a build and deploy jobs.  You can read all about the various aspects of GitHub Actions in their docs with regard to jobs and other syntax as I won’t try to expound upon that in this article.  Here is my full YAML for the entire workflow with some key areas highlighted:

name: .NET Core Build and Deploy

on: [push]

env:
  AZURE_RESOURCE_GROUP: blazor-deployment-samples
  BLOB_STORAGE_ACCOUNT_NAME: timheuerblazorwasm

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: Setup .NET Core
      uses: actions/[email protected]
      with:
        dotnet-version: 3.1.100

    - name: Build with dotnet
      run: dotnet build --configuration Release
    
    - name: Publish with dotnet
      run: dotnet publish --configuration Release 
    
    - name: Publish artifacts
      uses: actions/[email protected]
      with:
        name: webapp
        path: bin/Release/netstandard2.1/publish/BlazorApp27/dist

  deploy:
    needs: build
    name: Deploy
    runs-on: ubuntu-latest
    steps:

    # Download artifacts
    - name: Download artifacts
      uses: actions/[email protected]
      with:
        name: webapp

    # Authentication
    - name: Authenticate with Azure
      uses: azure/[email protected]
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS  }}

    # Deploy to storage using CLI
    - name: Deploy to storage using CLI
      uses: azure/[email protected]
      with:
        azcliversion: latest
        inlineScript: | 
          # show azure account being used
          az account show
          # az storage account upload
          az storage blob upload-batch --account-name ${{ env.BLOB_STORAGE_ACCOUNT_NAME }} -s webapp -d \$web
          # az set content type of wasm file until this is fixed by default from Azure Storage
          az storage blob update --account-name ${{ env.BLOB_STORAGE_ACCOUNT_NAME }} -c \$web -n _framework/wasm/mono.wasm --content-type application/wasm

So a few things going on here, let’s talk about them.

  • Lines 5-7: these are ‘local’ environment variables I set up.  The storage account name is NOT the blob container name but the actual storage account name.  This ideally probably should be a Secret as mentioned above.  Environment variables can be set here and then placeholders reference them later.
  • Starting at line 9 is where the ‘build’ portion is here.  We checkout the code, acquire the SDK and run the build and publish commands.  On line 26-30 is a step where we put the publish output to a specific artifact location for later retrieval of steps.  This is good practice.
  • Lines 40-42 is where we are now in the ‘deploy’ step of our CD and we retrieve those artifacts we previously pushed and we set them as a name ‘webapp’ that the later will use in deployment
  • Line 45 is where we are going to first authenticate to Azure using our service principal retrieved from the Secrets.  The ‘secrets’ object is not something you have to define and is part of the workflow so you just add the property you want to retrieve
  • Line 51 is where we start the deployment to Azure using the CLI commands and our param ‘webapp’ as the source.  This is the CLI command for uploading batch to storage as described in the docs for `az storage blob upload-batch`
  • Line 61 is an additional step that we need for .wasm files.  I believe this to be a bug because there is logic in the CLI to correctly map the content-type but for some reason it is not working…so for now you need to set the content-type for .wasm to `application/wasm` or the Blazor app will not work

This is made possible through a series of actions: checkout, dotnetcore, azure…all brining their functionality we can draw on and configure.  There are a bunch of Azure GitHub Actions we just released for specific tasks like deploying to App Service and such.  These don’t require CLI commands but rather just provide parameters to configure.  Because there is no Storage specific Action as of now, we can use the default CLI action to script what we want.  It is an enabler in lieu of a more strongly-typed action.  Now that we have this workflow YAML file complete we can commit and push to the repository.  In doing that we now have a CI/CD action that will trigger on any push (because that’s how we configured it).  We can see this action happening in my sample repo and you can see since we separate it in two jobs it will show them separately:

Screenshot of action deployment log

Summary

So now we have it complete end-to-end.  And subsequent check-in will trigger the workflow and deploy the bits to my storage account and I can now use my Azure Storage account as a host for my static website built on Blazor WASM.  This full YAML sample flow is available on my repo for this and you can examine it in more detail.

I would love to know how y’all are coming along using GitHub Actions with your .NET projects.  Please comment below!