This is the YAML I utilize for deploying an Angular App to Blob Storage. Under "capabilities" of the Storage Account, you will need to enable "Static Website" which will create the $web container, beyond that, update the variables in the following YAML and it should work as-is.
Here is the entire YAML, and then I will explain it section by section to give more context:
variables:
- name: storageContainer
value: myStorageaccount
- name: subscription
value: Subscription (8217a062-4d59-45f6-bc03-b6f559a1e58a)
trigger:
branches:
include:
- master
stages:
- stage: __default
jobs:
- job: Job
pool:
vmImage: windows-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.17.4'
force32bit: true
- task: CmdLine@2
displayName: npm install
inputs:
script: |
npm install
- task: CmdLine@2
displayName: ng build
inputs:
script: |
set PATH=%PATH%;%CD%\node_modules\.bin;C:\npm\prefix
ng build --subresource-integrity --output-hashing none
- task: AzureFileCopy@2
displayName: Deploy Files
inputs:
SourcePath: '$(System.DefaultWorkingDirectory)\dist\$(Build.Repository.Name)'
azureSubscription: $(subscription)
Destination: 'AzureBlob'
storage: $(storageContainer)
ContainerName: '$web'
BlobPrefix: ''
- task: AzureCLI@2
displayName: Delete Old Files
inputs:
azureSubscription: $(subscription)
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: 'az storage blob delete-batch -s `$web --account-name $(storageContainer) --if-unmodified-since $(Get-Date ((Get-Date).addMinutes(-5)) -UFORMAT +%Y-%m-%dT%H:%MZ)'
Okay, here it is section by section:
The following sets the values for the variables storageContainer and subscription to be used in the rest of the commands. Variables are replaced when you use $(storageContainer) type syntax.
variables:
- name: storageContainer
value: myStorageaccount
- name: subscription
value: Subscription (8217a062-4d59-45f6-bc03-b6f559a1e58a)
The following sets continuous build / delivery when code is returned to the master branch, and tells everything to deploy to a vm with the latest build of Windows. Windows is needed for some of the azure cli commands later on:
trigger:
branches:
include:
- master
stages:
- stage: __default
jobs:
- job: Job
pool:
vmImage: windows-latest
The following task installs node version 14.17.4 onto the system in order to utilize it. I had it utilize the 32-bit version of node, but that really shouldn't matter.
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.17.4'
force32bit: true
The following executes the npm install and installs everything from packages.json. I added the angular/cli to my dev dependencies, and therefore its installed with this. If you don't want to do that, you'll need a command that installs the angular cli. Please keep in mind, Azure DevOps doesn't seem to like it when there is more than one node install within a CmdLine block, so you'll need another block.
- task: CmdLine@2
displayName: npm install
inputs:
script: |
npm install
The following does the Angular build (ng build). The first thing I do is set the path appropriately so that it can find the Angular cli in the dev dependencies. I also have it add c:\npm\prefix to the path, which is apparently where the global npm installs are put. That was hard to find / figure out; so I left it in the path when I found it. In my Angular build I have two flags. The --subresource-integrity flag makes it so that the script tags have an integrity attribute that has the hash of the JavaScript file. This is considered a security best practice. I also have the --output-hashing none set. This makes it so that files are just main.js rather than the longer names with the hash in the JS file. This is just a preference of mine.
- task: CmdLine@2
displayName: ng build
inputs:
script: |
set PATH=%PATH%;%CD%\node_modules\.bin;C:\npm\prefix
ng build --subresource-integrity --output-hashing none
The following Azure Cli command copies all of the files to the $web container of the storage account. The Angular build command by default putss these files from whatever is listed in "outputPath" in your angular.json. By default this is a folder under the dist directory. That folder happens to be the same exact name as my Git Repository, which I think is normal. This is why I have the source to grab files from be '$(System.DefaultWorkingDirectory)\dist$(Build.Repository.Name)' . I think this should work for you out-of-the-box the way I have it, but if not, replace "dist$(Build.Repository.Name)" with whatever is set in outputPath of your angular.json .
- task: AzureFileCopy@2
displayName: Deploy Files
inputs:
SourcePath: '$(System.DefaultWorkingDirectory)\dist\$(Build.Repository.Name)'
azureSubscription: $(subscription)
Destination: 'AzureBlob'
storage: $(storageContainer)
ContainerName: '$web'
BlobPrefix: ''
When copying files to an Azure Storage Account, by default it will overwrite files with the same name. However, it won't do anything with files that are no longer used and aren't overwritten. For that reason, this last section will delete any files that haven't been modified in the last 5 minutes. This build process on average takes about 4 minutes, so I figured anything that was older than 5 minutes was from a prior build.
- task: AzureCLI@2
displayName: Delete Old Files
inputs:
azureSubscription: $(subscription)
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: 'az storage blob delete-batch -s `$web --account-name $(storageContainer) --if-unmodified-since $(Get-Date ((Get-Date).addMinutes(-5)) -UFORMAT +%Y-%m-%dT%H:%MZ)'
Hopefully this helps save someone some time. This took MUCH more time than I was expecting to get properly put together.