November 19th, 2020

Static Web App PR Workflow for Azure App Service using Azure DevOps Pt 2 (But what if my code is in GitHub)

Abel
Principal Cloud Advocate, DevOps Lead

Static Web App PR Workflow for Azure App Service using Azure DevOps Pt 2 (But what if my code is in GitHub)

In part 1 (Static Web App PR Workflow for Azure App Service), I walked you you through how to set up that sweet pull request workflow for Static Web Apps for your app if your app was:

But what if your code is GitHub and your CI/CD Pipeline is using Azure Pipelines? Easy peasy. Just continue reading and I’ll show you what to do.

Again, like that first blog post of mine, I’ll show the classic editor first because it’s prettier than a wall of YAML. But if you want the TLDR version, I’ll post the YAML for my CI pipeline at the end of the blog post.

What Needs To Change?

Luckily, not much has to change. All you need to do is make two small tweaks.

  • Update the task, Add Staging URL to PR Comment
  • Decide what you want to happen with pull requests from forks.

The first tweak is simple enough. Instead of using the Azure DevOps REST API to add the staging URL to the PR comment, we will now use the GitHub REST API for Pull Requests.

For the second tweak, there are some security concerns to think about and a couple of different ways to work through them.

Changing the task, Add Staging URL to PR Comment

Last time, by the time we were done, our CI pipeline looked like this with a task named Add Staging URL to PR Comment:

Image c27768d13821c76d5dd1a7dccbbe25922b9cab97cc53e8911d494a218318facd

Our inline code for that task looked like this:

# Add PR comment with link to staging
echo "Adding PR comment with link to staging environment..."
message="Created staging environment for PR - https://$(webApp.name)-staging-pr-"$(System.PullRequest.PullRequestId)".azurewebsites.net/"
echo "message: $message"

# post message to PR using azure devops rest api
curl -i --user :$(azureDevOpsPAT) -H "Content-Type: application/json" -H "Accept: aon" -X POST -d '{"comments": [{ "parentCommentId":0, "content": "'"$message"'", "commentType": 1}], "status": 1}' $(System.TeamFoundationCollectionUri)/$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullRequests/$(System.PullRequest.PullRequestId)/threads?api-version=6.1-preview.1

echo "Done adding PR comment with link to staging environment"

This script uses the Azure DevOps REST API to dynamically add a message with the staging url to the pull request comment in Azure Repos.

Since our code is in GitHub, we need to change the script so it uses the GitHub REST API for Pull Requests. In order for that to work, GitHub recommends using an OAuth2 token sent in the authorization header. The easiest way to do that is to create and use a GitHub personal access token (PAT).

Generating a GitHub Personal Access Token

To create a PAT, log into GitHub and then from your picture in the upper right hand corner click Settings

Image 9fcf546f73421355eae6b396f6148bbdf3dd0df3bdebfba92cc3d2b989a8227a

Developer settings

Image 9fe7b4bd0b7725a321632f1ad0fba84a622ab11fb57cb964df058e2f32790c75

Personal Access Token

Image 6e3f39e857e42ae5154e810169473c7ee609a9bf2ec72430a29e92127f03079f

Generate new token

Image 80235ac33bcf1dc1c8be325bab21ca8fd455260bc7fd53c5b8e00ed40b00f89b

And from here, you can create your PAT. Make sure you save this value because once you leave this page, you’ll never get this value again!

Create New Task, Add Staging URL to PR Comment, To Add Message To GitHub PR

We’re gonna be using this GitHub PAT in our pipeline so let’s add this as a secure pipeline variable.

Image 0e848084dc5ceb6e81d0ab91f25e9e9d0c02e47acb268e26bbae8a4b5b3460f7

Next, I delete the bash task named Add Staging URL to PR Comment and in its place I add a powershell task. Why powershell? No reason. I just wanted to show it really doesn’t matter what shell you use. Use what works for you!

This powershell task should only run if all the previous tasks were successful and the build was triggered by a pull request. So I set Control Option > Run this task to

Custom conditions

and I set Control Option > Custom condition to

and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))

Image e6f619b025d482b720a78c6c9caf045d943129c2f51c60d6e36e5dc1cb533220

Next, I give this task a better name

Add Staging URL to PR Comment

select

Inline

and in the Script textbox, I add the powershell script which uses the GitHub REST API for Pull Requests to add the comment to the pull request.

Write-Output "Adding comment to GitHub pull request via rest api..."

# create GitHub REST api URL
$prNumber = $(System.PullRequest.PullRequestNumber)
$prInfoUrl = 'https://api.github.com/repos/abelsquidhead/TestPRTriggerRepo/pulls/' + $prNumber
$response = Invoke-RestMethod -URI $prInfoUrl
$commentUrl = $response.issue_url + '/comments'

# add comment to the PR in GitHub using GitHub REST api
$authorizationHeaderValue = "token " +  $env:GITHUB_OATH_TOKEN
$message = "Created staging environment for PR - https://abeltestwebapp-staging-pr-" + $prNumber + ".azurewebsites.net/"
$body = '{"body":"' + $message + '"}'
Invoke-RestMethod -Method 'Post' -Uri $commentUrl -Headers @{"Authorization" = $authorizationHeaderValue} -Body $body
Write-Output "Added staging url as comment to GH pull request"

Image 2530fb1c6910929b09832e5808417e93dc3ca6be1e718cb523c99a883a8218ac

Notice in the inline script, I’m adding authentication through my header

$authorizationHeaderValue = "token " +  $env:GITHUB_OATH_TOKEN

I’m passing my GitHub PAT in as an environment variable so I’ll need to set it under Environment Variables:

Image 72d6bc27ed4179ad63795e18a5c126dd50e9e5e02ce777301e402101f10bc7f8

Enabling CI for pull requests from GitHub

To enable this CI build to run on every pull request, go Triggers > Pull request validation and click Enable pull request validation

Image f4e99f3aa912897649d0fa3e634aeba3ca7b76b43d3e5b13dd6a21a2098fffb7

Next we need to decide if we want this CI build to run on every pull request, including Forks.

Security Considerations for Forks and CI Builds

If we want this PR workflow to run on all pull requests including forks, just click on Build pull requests from forks of this repository and check Make secrets available to builds of forks. That’s because our CI pipeline uses secrets.

Image 08bc8a44c26286b31ae33eba685c011a2f51ced5ab97fb3bf7812e4a4ae8ba5b

However, before you do this, you need to think about security. Forks from GitHub can really come from anywhere! They can even come from non trusted people, or even worse, malicious people. If all pull requests including PR’s from forks automatically run this CI pipeline, some simple changes to unit tests or to other scripts could easily leak your secrets (most of my ci pipelines automatically run my unit tests). Yikes!!!!

One thing you can do is tweak your CI pipeline so all it does is compile, run tests and package everything up. And then create a CD pipeline that only gets triggered after a successful CI run that does the provision PR staging slot, deploy to slot and add staging slot URL to PR comment. Triggering one pipeline after another is super useful. This would make leaking secrets a tougher since now, only your CD pipeline uses secrets. I can’t just make malicious changes to unit tests that will automatically get run. However, I could still make some malicious changes in the code itself and secrets could still be leaked. That might be good enough though.

To be the most secure, don’t automatically execute this workflow from forks. Manually trigger the CI for PR’s from forks. This will give you the opportunity to review the code before automatically triggering the build. Read this for more details on security considerations with forks.

YAML Time

See, I told you. Easy peasy. So what does all this look like in YAML?

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

variables:
  servicePrincipal.name: 'http://AbelDeployDemoBackupPrincipal'
  servicePrincipal.tenant: '72f988bf-86f1-41af-91ab-2d7cd011db47'
  webApp.name: 'PRWorkflowBlogAppService'
  webApp.resourceGroup: 'PRWorkflowBlog'
  BuildConfiguration: 'release'

steps:

# Use .NET core 5 sdk
- task: UseDotNet@2
  displayName: 'Use .NET Core 5 SDK'
  inputs:
    version: 5.x
    includePreviewVersions: true

# Build .net core project    
- task: DotNetCoreCLI@2
  displayName: Build
  inputs:
    projects: '$(Parameters.RestoreBuildProjects)'
    arguments: '--configuration $(BuildConfiguration)'

# Restore project
- task: DotNetCoreCLI@2
  displayName: Build
  inputs:
    projects: '$(Parameters.RestoreBuildProjects)'
    arguments: '--configuration $(BuildConfiguration)'

# Run unit tests
- task: DotNetCoreCLI@2
  displayName: Test
  inputs:
    command: test
    projects: '$(Parameters.TestProjects)'
    arguments: '--configuration $(BuildConfiguration)'

# Publish .net core app to staging directory
- task: DotNetCoreCLI@2
  displayName: Publish
  inputs:
    command: publish
    publishWebProjects: True
    arguments: '-r win-x64 --self-contained true --configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'

# Publish build bits as build artifact
- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(build.artifactstagingdirectory)'
  condition: succeededOrFailed()

# Create CI Staging Slot for PR
- bash: |
   # Login to Azure using service principal
   echo 'Logging into Azure with service principal...'
   az login \
     --service-principal \
     --username $(servicePrincipal.name) \
     --tenant $(servicePrincipal.tenant) \
     --password $SERVICE_PRINCIPAL_PASSWORD
   echo 'Logged into Azure'
   echo ''

   # Create staging slot for PR
   echo 'Creating staging slot for PR...'
   az webapp deployment slot create \
       --name $(webApp.name) \
       --resource-group $(webApp.resourceGroup) \
       --slot staging-pr-$(System.PullRequest.PullRequestId)
   echo "Created staging slot for PR " $(System.PullRequest.PullRequestId)

  displayName: 'Create CI Staging Slot For PR'
  condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
  env:
    SERVICE_PRINCIPAL_PASSWORD: $(servicePrincipal.password)

# Add staging URL to PR comment
steps:
- powershell: |
  Write-Output "Adding comment to GitHub pull request via rest api..."

  # create GitHub REST api URL
  $prNumber = $(System.PullRequest.PullRequestNumber)
  $prInfoUrl = 'https://api.github.com/repos/abelsquidhead/TestPRTriggerRepo/pulls/' + $prNumber
  $response = Invoke-RestMethod -URI $prInfoUrl
  $commentUrl = $response.issue_url + '/comments'

  # add comment to the PR in GitHub using GitHub REST api
  $authorizationHeaderValue = "token " +  $env:GITHUB_OATH_TOKEN
  $message = "Created staging environment for PR - https://abeltestwebapp-staging-pr-" + $prNumber + ".azurewebsites.net/"
  $body = '{"body":"' + $message + '"}'
  Invoke-RestMethod -Method 'Post' -Uri $commentUrl -Headers @{"Authorization" = $authorizationHeaderValue} -Body $body
  Write-Output "Added staging url as comment to GH pull request"
  displayName: 'Write Staging URL to PR as Comment'
  condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
  env:
    GITHUB_OATH_TOKEN: $(github.oathToken)

# Deploy build to staging environment
- task: AzureRmWebAppDeployment@4
  displayName: 'Deploy to PR Staging'
  inputs:
    ConnectionType: 'AzureRM'
    azureSubscription: 'AbelDemosSubscription(2f42eaaf-1883-462f-a04f-a1d88fa6fd44)'
    appType: 'webApp'
    WebAppName: 'PRWorkflowBlogAppService'
    deployToSlotOrASE: true
    ResourceGroupName: 'PRWorkflowBlog'
    SlotName: 'staging-pr-$(System.PullRequest.PullRequestId)'
    packageForLinux: '$(System.DefaultWorkingDirectory)/**/netlandingpage.zip'
    enableCustomDeployment: true
    DeploymentType: 'webDeploy'
    RemoveAdditionalFilesFlag: true
    ExcludeFilesFromAppDataFlag: false
  condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))




# Delete staging slot
- bash: |
   # PR Closed, changes merged to main
   echo 'PR close, changes merged to main'

   # get the source version message
   sourceVersionMessage="$(Build.SourceVersionMessage)"
   echo 'build source version message: ' $sourceVersionMessage

   # parse the PR number from the source version message
   prNumber=$(echo $sourceVersionMessage| sed -r 's/^([^.]+).*$/\1/; s/^[^0-9]*([0-9]+).*$/\1/')
   echo 'prNumber: ' $prNumber

   # Login to Azure using service principal
   echo 'Logging into Azure with service principal...'
   az login \
     --service-principal \
     --username $(servicePrincipal.name) \
     --tenant $(servicePrincipal.tenant) \
     --password $SERVICE_PRINCIPAL_PASSWORD
   echo 'Logged into Azure'
   echo ''

   # Delete PR staging slot
   echo 'Deleting staging slot for PR ' $prNumber '...'
   az webapp deployment slot delete \
     --name $(webApp.name) \
     --resource-group $(webApp.resourceGroup) \
     --slot staging-pr-$prNumber
   echo 'Deleted slot for PR ' $prNumber
  displayName: 'Delete Staging Slot For PR'
  condition: and(succeeded(), startsWith(variables['Build.SourceVersionMessage'], 'Merged PR '))
  env:
    SERVICE_PRINCIPAL_PASSWORD: $(servicePrincipal.password)

To set the trigger, go to Edit

Image 8cd3440fbb1a4110872fb601fffe4714f0aed80cfbd97d9deb0229f4fb4673cd

3 virticle dots > Triggers

Image df44bfbb03b243eee5aeeae76c1ee81e55120225560ecfb556fe16e6c9f9db1f

Pull request validation

Image 24631782cd4c8999f63a53e293b6458d6812099c983108e464614850fb85bdb2

Remember, give some serious consideration to how you want forks to be handled. There are definitely security concerns to be thought through.

Conclusion

The Pull Request workflow used by Static Web Apps is sick. In part 1 of this series (Static Web App PR Workflow for Azure App Service), I showed how to do this same workflow for Azure App Service, Azure Repos and Azure Pipelines. For this blog, I showed how to modify that pipeline if your code sits in GitHub instead of Azure Repos.

The changes were pretty simple. Just swap out the Azure DevOps REST API calls to write a pull request comment for the GitHub REST API for Pull Requests.

Because our code now sits in GitHub, some extra security considerations should be given to pull requests coming from Forks. But beyond that, it’s easy enough to tweak our CI definition in Azure Pipelines to handle having our code in GitHub.

Related Links

(Static Web App PR Workflow for Azure App Service)

Static Web Apps

GitHub Actions

Azure App Service

Azure Repos

Azure Pipelines

CI/CD GitHub Repositories with Azure Pipelines

.NET Core

Branch Policies

Staging with Deployment Slots

Azure CLI

Login to Azure

Creating Slots with Azure CLI

Azure Service Principal

Pipeline Built In Variables

Azure DevOps REST API

Write Comment To Pull Request

Azure DevOps PAT

Azure DevOps Web Hooks

Azure Functions

Delete Slot with the Azure CLI

Using Secret Variables In YML Pipelines

GitHub REST API for Pull Requests

Trigger one pipeline after another

Security considerations with forks

Author

Abel
Principal Cloud Advocate, DevOps Lead

Abel Wang is a Principal Cloud Advocate and DevOps Lead at Microsoft, specializing in DevOps and Azure with a background in application development. He is currently part of Donovan Brown's League of Extraordinary Cloud DevOps Advocates. Before joining Microsoft, Abel spent seven years as a Process Consultant and a Certified Scrum Master helping customers globally develop solutions using agile practices and Team Foundation Server. Prior to that, Abel founded and sold his own software company. ...

More about author

0 comments

Discussion are closed.