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:
- hosted in Azure App Service
- your code in Azure Repos
- your CI pipeline in Azure Pipelines.
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
:
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
Developer settings
Personal Access Token
Generate new token
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.
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'))
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"
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:
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
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.
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
3 virticle dots > Triggers
Pull request validation
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)
CI/CD GitHub Repositories with Azure Pipelines
Delete Slot with the Azure CLI
Using Secret Variables In YML Pipelines
GitHub REST API for Pull Requests
0 comments