At Black Marble, we have had a long standing Azure DevOps Team Project that we used for end-to-end demos of the principles of DevOps called Living the Dream. This used a legacy codebase, the old Microsoft Fabrikam demo, and showed that can be deployed using modern tools.

As I had no similar demo for GitHub Enterprise, I thought it would be interesting to see how the migration process taking my Azure DevOps implementation over to GitHub would go. This is a good learning exercise as it is the type of problem that many of our enterprise clients will need to do if changing DevOps platform. My key aim was to do the minimum to get the CI/CD process moved from Azure Pipelines to GitHub Action

Moving the source

This was easy, I just imported the Git repo from Azure DevOps

Build

The build for my solution was fairly easy, the project is a pair of Visual Studio solutions, one for the ARM code and the other for the Website code.

The key point was to make sure I passed in the correct parameters to make sure the web site was packaged up using WebDeploy

  Build-Solution:
    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v3

    - name: Add MSBuild to PATH
      uses: microsoft/setup-msbuild@v1.0.2

    - name: Restore NuGet packages
      working-directory: ${{env.GITHUB_WORKSPACE}}
      run: nuget restore ${{env.SOLUTION_FILE_PATH}}

    - name: Build Solution
      working-directory: ${{env.GITHUB_WORKSPACE}}
      # Add additional options to the MSBuild command line here (like platform or verbosity level).
      # See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference
      run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} /p:DeployOnBuild=true /p:PublishProfile=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}

Deployment

The majority of the work was required to get the solution deployed i.e.

  • Creating all the required Azure resources using an ARM template
  • Deploying the website via MSDeploy.

In Azure DevOps I had tasks to manage these steps, for GitHub actions, though some actions exist, I also had to write some scripts.

I ended up putting the Actions required for the ARM and Solution deployment in reusable workflows, so I could call the steps at multiple locations on my workflows (for the test deployment and the production deployment) without repeating actions.

ARM

The workflow for the ARM was as follows. Nothing that special, the key points to note are

  • You have to use the Azure Login action, this in effect replaces the Azure Pipelines service connection.
  • I have to use a script to check the Azure resource group exists prior to the deployment. In Azure Pipelines the ARM task will create the resource group if not present, but this is not so with the GitHub Action
  • I inject all my ARM parameters as inline parameters (as opposed to a file), this was just to keep the same pattern as had been used on Azure DevOps
name: Reuseable workflow to publish ARM to Azure resource group

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    # all secrets inherited    
jobs:
  Integration-ARM:
    runs-on: windows-latest
    environment:
      name: ${{ inputs.environment }}
    steps:
    - name: Download ARM Build Artifact
      uses: actions/download-artifact@v3.0.1
      id: arm-download
      with:
        # Artifact name
        name: ARM
        path: ./ARM

    - name: Azure Login
      uses: Azure/login@v1.4.6
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}     

    - name: Ensure Azure Resource Group is created
      run: |
        $rgexists = az group exists -n ${{ secrets.AzureResourceGroup}}
        if ($rgexists -eq 'false') {
          az group create --name ${{ secrets.AzureResourceGroup}} --location ${{ secrets.AZURELOCATION }}
          write-host "Creating Azure Resource Group"
        }        
      shell: pwsh 

    - name: Deploy Azure Resource Manager (ARM) Template
      uses: Azure/arm-deploy@v1.0.8
      with:
        scope: 'resourcegroup'
        subscriptionId: ${{ secrets.AZURESUBSCRIPTION }}
        region: ${{ secrets.AZURELOCATION }}
        resourceGroupName: ${{ secrets.AzureResourceGroup}}
        template: '.\\ARM\\Templates\\WebSiteSQLDatabase.json' 
        deploymentMode: 'Incremental'
        parameters: 'hostingPlanName="${{ secrets.HostingPlanName}}" hostingPlanSku="${{ secrets.hostingPlanSku}}" hostingPlanCapacity="${{ secrets.hostingPlanCapacity}}" webSiteName="${{ secrets.Sitename}}" sqlserverName="${{ secrets.sqlservername}}" sqlServerAdminLogin="${{ secrets.SQLUser}}" sqlServerAdminPassword="${{ secrets.SQLPassword}}" databaseName="${{ secrets.databasename}}" collation="SQL_Latin1_General_CP1_CI_AS" edition="Standard" maxSizeBytes="1073741824" requestedServiceObjectiveName="S0" appInsightsLocation="${{ secrets.AzureLocation}}" VersionTag="1.2.3"" DeploymentDate="2022-10-28"" EnvironmentTag="tag"'

Solution

The MSDeploy was more problematic. It is fair to say this is not a currently fashionable technology. Web deploy has very much moved to the ‘copy a zip file’ approach. There was no GitHub Action available to run MSDeploy against Azure, so I had to work out the parameters and script it.

Again the key points to note

  • I could not find a GitHub Action that would automatically update a configuration file replacing tokens found with values from environment variables and secrets. There a few that do part of the job, but not it all. This is something I might well write, but for now a script that replaces the tokens did the job - and yes I know it is probably better practice to set these values in the Azure WebApp directly, but as I said I was trying for a like for like replacement.
  • The MSDeploy relies on pulling down the publish profile then calling the MSDeploy EXE which is present on the GitHub Agent. This took a while to get right, but once it was done, it is reliable
name: Reuseable workflow to publish web solution to Azure WebApp

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    # all secrets inherited    
jobs:
  Integration-ARM:
    runs-on: windows-latest
    environment:
      name: ${{ inputs.environment }}
    steps:
    - name: Download Web Deploy Solution Build Artifact
      uses: actions/download-artifact@v3.0.1
      with:
        # Artifact name
        name: 'Webdeploy-Package'
        path: ./WebDeploy

    - name: Replace tokens in configuration file
      run: |
        $file = ".\WebDeploy\FabrikamFiber.Web.SetParameters.xml"
        $filecontent = Get-Content -Path $file
        $filecontent = $filecontent -replace "__Sitename__", "${{secrets.Sitename}}" 
        $filecontent = $filecontent -replace "__LOCATION__", "${{secrets.LOCATION}}" 
        $filecontent = $filecontent -replace "__GENERATETESTDATA__", "${{secrets.GENERATETESTDATA}}" 
        $filecontent = $filecontent -replace "__sqlservername__", "${{secrets.sqlservername}}" 
        $filecontent = $filecontent -replace "__databasename__", "${{secrets.databasename}}" 
        $filecontent = $filecontent -replace "__SQLUser__", "${{secrets.SQLUser}}" 
        $filecontent = $filecontent -replace "__SQLPassword__", "${{secrets.SQLPassword}}"
        $filecontent | Out-File $file
        cat $file                  
      shell: pwsh

    - name: Azure Login
      uses: Azure/login@v1.4.6
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}     

    - name: 'Deploy web site with MSDeploy'
      run: |
        $publishProfile = az webapp deployment list-publishing-profiles --resource-group ${{ secrets.AzureResourceGroup}} --name ${{ secrets.Sitename }} --query "[?publishMethod=='MSDeploy']" --subscription "${{ secrets.AZURESUBSCRIPTION}}" | convertfrom-json
        $shortPath = (New-Object -ComObject Scripting.FileSystemObject).GetFolder("./WebDeploy").ShortPath  
        & "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe" -verb:sync -source:package="$shortpath\FabrikamFiber.Web.zip" -setParamFile:"$shortpath\FabrikamFiber.Web.SetParameters.xml" -dest:auto,ComputerName="https://$($publishProfile.msdeploySite).scm.azurewebsites.net/msdeploy.axd?site=$($publishProfile.msdeploySite)",UserName=$($publishProfile.userName),Password=$($publishProfile.userPWD),AuthType='Basic' -verbose -debug -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension        
      shell: pwsh

Summary

So I now have the core of my ‘Living the Dream’ demo on GitHub, the is more of course I can do, but it is a good start and has been a good learning experience.

This form of activity is something I would recommend to anyone trying to get their had around the intricacies of GitHub, or any technology new to them. You always learn more I think when trying to do your own project as opposed to just following a lab.