Azure Pipelines: How to build and test an Angular and Node.js webapp

Deploy Angular and Node.js webapp in Azure Pipelines (part 5)

In the previous article I explained how to set up multiple environments for Azure Pipelines. In this article I will explain how you can deploy the artifacts to the on-premise VM’s. This are the artifacts you build in the pipeline. I am using an template for this, because we are using the same script three times. An template avoids code repetition.
I have three stages with a deployment job:

  • acceptance
  • test
  • production.

By defining the name of the environment it knows to which VM it needs to deploy. You can add a job with a task by adding the following to the azure-pipelines.yml

- stage: deployAcceptatie
displayName: Deploy Acceptatie
dependsOn: 
- Build
jobs:
  - deployment: VMDeploy_Acceptatie
    displayName: deploy Acceptatie
    environment:
      name: Service Portaal VM acceptatie
      resourceType: VirtualMachine
      tags: acceptatie
    strategy:
      runOnce:
        deploy:
          steps:
            - template: deploy.yml

Deploy with the deployment template

We use the task template. Create a new file called deploy.yml. I am using pm2 to run the node application. You could also use some other service like docker. This is very dependent on your VM and how your VM is configured (or what you want to use).

steps:
  - download: current
    displayName: Download backend
    artifact: 'backend'
  - task: DownloadPipelineArtifact@2
    displayName: Download frontend
    inputs:
      artifact: 'frontend'
      path: '$(Pipeline.Workspace)/frontend$'
      patterns: 'dist/**'
  - task: Bash@3
    displayName: Stop application
    inputs:
      targetType: 'inline'
      script: |
          export HOME=/home/pm2
          chown -R pm2:pm2 /home/azure_pipeline/
          su -c "pm2 stop all" -m "pm2"
      workingDirectory: '/opt/backend'
  - task: Bash@3
    displayName: Backend deploy
    inputs:
      targetType: 'inline'
      script: |
          mv -v $(Pipeline.Workspace)/backend/dist ./dist
          mv -v $(Pipeline.Workspace)/backend/node_modules ./node_modules
      workingDirectory: '/opt/backend'
  - task: Bash@3
    displayName: Frontend deploy
    inputs:
      targetType: 'inline'
      script: |
          mv -v $(Pipeline.Workspace)/frontend/* ./dist
      workingDirectory: '/opt/frontend'
  - task: Bash@3
    displayName: Start application
    inputs:
      targetType: 'inline'
      script: |
          export HOME=/home/pm2
          su -c "pm2 start all" -m "pm2"
      workingDirectory: '/opt/backend'

The highlighted code is the code specific to your VM. Do this for all the three environments. The final azure-pipelines.yml will look like this:

trigger:
  - master
  
pool:
  vmImage: 'ubuntu-latest'
  
stages:
  - stage: Build
    jobs:
      - job: Frontend
        steps:
          - task: Npm@1
            inputs:
              command: 'ci'
              workingDir: '$(System.DefaultWorkingDirectory)/frontend'
          - task: Bash@3
            inputs:
              targetType: 'inline'
              script: 'npm run ng build'
              workingDirectory: '$(System.DefaultWorkingDirectory)/frontend'
          - task: DeleteFiles@1
            displayName: 'Delete JUnit files'
            inputs:
              SourceFolder: '$(System.DefaultWorkingDirectory)/frontend/junit'
              Contents: 'TESTS*.xml'
          - task: Npm@1
            displayName: 'Test Angular'
            inputs:
              command: custom
              customCommand: run test -- --watch=false --code-coverage
              workingDir: '$(System.DefaultWorkingDirectory)/frontend'
          - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
            displayName: ReportGenerator
            inputs:
              reports: '$(Build.SourcesDirectory)/frontend/coverage/cobertura-coverage.xml'
              targetdir: '$(Build.SourcesDirectory)/frontend/coverage'
              reporttypes: 'Cobertura'
              assemblyfilters: '-xunit*'
          - task: PublishTestResults@2
            displayName: 'Publish Angular test results'
            condition: succeededOrFailed()
            inputs:
              testResultsFormat: 'JUnit'
              testResultsFiles: '$(System.DefaultWorkingDirectory)/frontend/junit/TESTS-*.xml'
              searchFolder: '$(System.DefaultWorkingDirectory)/frontend/junit'
              testRunTitle: 'Frontend mocha tests'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.SourcesDirectory)/frontend/coverage'
              artifact: 'frontendCoverage'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Pipeline.Workspace)/frontend'
              artifact: 'frontend'
              publishLocation: 'pipeline'


      - job: Backend
        steps:
          - task: Npm@1
            inputs:
              command: 'ci'
              workingDir: '$(System.DefaultWorkingDirectory)/backend'
          - task: Bash@3
            inputs:
              targetType: 'inline'
              script: 'npm run build'
              workingDirectory: '$(System.DefaultWorkingDirectory)/backend'
          - task: Npm@1
            displayName: run test backend report
            inputs:
              command: 'custom'
              workingDir: 'backend'
              customCommand: 'run test-azure-report'
          - task: Npm@1
            displayName: run test backend coverage
            inputs:
              command: 'custom'
              workingDir: 'backend'
              customCommand: 'run test-azure-coverage'
          - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
            displayName: ReportGenerator
            inputs:
              reports: '$(Build.SourcesDirectory)/backend/coverage/cobertura-coverage.xml'
              targetdir: '$(Build.SourcesDirectory)/backend/coverage'
              reporttypes: 'Cobertura'
              assemblyfilters: '-xunit*'
          - task: PublishTestResults@2
            inputs:
              testResultsFormat: 'JUnit'
              testResultsFiles: 'junit.xml'
              searchFolder: '$(Build.SourcesDirectory)/backend'
              testRunTitle: 'Run backend jest tests'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.SourcesDirectory)/backend/coverage'
              artifact: 'backendCoverage'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Pipeline.Workspace)/backend'
              artifact: 'backend'
              publishLocation: 'pipeline'


      - job: codeCoverage
        displayName: publish code coverage
        dependsOn:
          - Frontend
          - Backend
        steps:
          - checkout: none
          - task: DownloadPipelineArtifact@2
            inputs:
              artifact: backend        
              path: $(Pipeline.Workspace)/backend/backend
              itemPattern: '!node_modules/**'
          - task: DownloadPipelineArtifact@2
            inputs:
              artifact: frontend
              path: $(Pipeline.Workspace)/frontend/frontend
              itemPattern: '!node_modules/**'
          - download: current
            artifact: backendCoverage
          - download: current
            artifact: frontendCoverage
          - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
            displayName: ReportGenerator
            inputs:
              reports: '$(Pipeline.Workspace)/backendCoverage/Cobertura.xml;$(Pipeline.Workspace)/frontendCoverage/Cobertura.xml'
              targetdir: '$(Pipeline.Workspace)/coverage'
              reporttypes: 'HtmlInline_AzurePipelines;Cobertura'
          - task: PublishCodeCoverageResults@1
            inputs:
              codeCoverageTool: 'Cobertura'
              summaryFileLocation: '$(Pipeline.Workspace)/coverage/*.xml'
              reportDirectory: '$(Pipeline.Workspace)/coverage'

  - stage: deployAcceptance
    displayName: Deploy Acceptance
    dependsOn: 
    - Build
    jobs:
      - deployment: VMDeploy_Acceptance
        displayName: deploy Acceptance
        environment:
          name: Service Portaal VM acceptance
          resourceType: VirtualMachine
        strategy:
          runOnce:
            deploy:
              steps:
                - template: deploy.yml

  - stage: deployTest
    displayName: Deploy Test
    dependsOn: 
    - Build
    - deployAcceptatie
    jobs:
      - deployment: VMDeploy_Test
        displayName: deploy Test
        environment:
          name: Service Portaal VM test
          resourceType: VirtualMachine
        strategy:
          runOnce:
            deploy:
              steps:
                - template: deploy.yml

  - stage: deployProduction
    displayName: Deploy Production
    dependsOn: 
    - Build
    - deployAcceptance
    - deployTest
    jobs:
      - deployment: VMDeploy_Production
        displayName: deploy Production
        environment:
          name: Service Portaal VM productie
          resourceType: VirtualMachine
        strategy:
          runOnce:
            deploy:
              steps:
                - template: deploy.yml

Conditions

Now if you only want to deploy production with a push on master, we can add a condition. I am using the next condition.

- stage: deployProduction
  condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
  displayName: Deploy Production
  dependsOn: 
  - Build
  - deployAcceptance
  - deployTest
  jobs:
    - deployment: VMDeploy_Production
      displayName: deploy Production
      environment:
        name: Service Portaal VM productie
        resourceType: VirtualMachine
      strategy:
        runOnce:
          deploy:
            steps:
              - template: deploy.yml

And in your Azure Pipelines dashboard it should now look something like this. Some of the stages have been skipped here, because I didn’t want them to deploy yet.

This shows the deployment to the environments
This shows the runs that have been run
Here you can see the code coverage

Now you should have a pipeline in Azure Pipelines, with multiple deployment environment on self-hosted VM’s, using Node.js and Angular.
If you have any questions or suggestions, feel free to leave them below. I hope I helped some of you out!