How to continuously build and deploy a vue3 app, from a GitHub repo, using Azure Pipelines

I recently started the development of a vue3 application (web.replayer.app) and wanted to explore how to automatically (a.k.a continuously) build and deploy from my GitHub repository to my online test environment. Azure Pipelines seems to be an easy way to do it.

My experience is, if it builds locally, it also builds in the cloud. In fact, with a typical setup for a VueJs application, most parts are already in place. To use Azure Pipelines you additionally need to:

  • Have a DevOps organization
  • Create a new Pipeline with the “Node.js with Vue” template for your Repository
  • Make sure you have setup at least 1 Parallel Job
  • Optionally, add a test task and test report to the pipeline config
  • Add a deployment step

Create an Azure Pipeline

I already have an Azure subscription, but creating an Azure pipeline is free for a public GitHub repository anyway . I only needed to choose an organization name, create a project and follow this guide from Microsoft.

Following this path, you will need to give Azure quite some access to your Repository, with the OAuth authentication scheme, including admin access for webhooks, of course, but also to all repositories, including private ones, with write access.

Giving Azure Piplines access to your GitHub account.
Giving Azure Piplines access to your GitHub account.

To learn more about the specifics of access control and how to best structure the projects in Azure Dev Ops, read this article about how to Build GitHub repositories. There is also a section addressing the permission requests for the OAuth access as show in the image above.

Note: For me, the security policy of the newly created organization was set to not “Allow public projects”. You may want to change that, as it impacts your choices on parallel jobs later on.

  • After selecting the repository and installing the suggested GitHub App into your new pipeline for it, select “Node.js with Vue”:
Configuring the pipeline for Vue in Azure DevOps
Configuring the pipeline for Vue in Azure DevOps
  • Click “Save and run”, and commit the resulting yaml file to your repo.
  • Evaluate the results

If you are lucky enough you already get the green checkmark, indicating a successful build:

A successful run of an Azure Pipeline
A successful run of an Azure Pipeline

Parallel Jobs

Initially, I got an error saying

##[error]No hosted parallelism has been purchased or granted. To request a free parallelism grant, please fill out the following form https://aka.ms/azpipelines-parallelism-request

The problem is, in my case, the project was set up as private, and no Microsoft-hosted “Parallel jobs” were assigned, and no Self-hosted job was set up.

How to set up “Parallel jobs”

Depending on how your project is set up (private or public) and depending on your hosting requirements (Microsoft- or self-hosted), you need to set up parallel jobs. In any case you need at least one, even if you do not actually need to run multiple jobs at the same time in a pipeline.

Basically you have two options:

For public projects, with public source code from GitHub, you can apply for free grant, using the link mentioned in the error message. I did this and was approved the next day.

Testing

Within my project, I am using Jest as my JavaScript (unit) testing framework. To run the unit tests within Azure Pipelines and have published test results, I installed the jest-junit npm package and configured it like this in package.json:

    "scripts": {
        "test": "jest --ci --reporters=default --reporters=jest-junit",
    },

This creates the output as junit.xml in the root folder of the project. To actually have the Azure Pipeline publish the results I added a test task to the end of my azure-pipelines.yml file:

    - task: PublishTestResults@2
      condition: succeededOrFailed()
      inputs:
          testRunner: JUnit
          testResultsFiles: '**/junit.xml'

A successful run then publishes the test results alongside the pipeline’s summary tab:

Test summary for a successful pipeline run
Test summary for a successful pipeline run

Creating the artifacts

My project creates a dist folder, containing the complete web app, which can be deployed. I used another task to create an artifact at the end of my azure-pipelines.yml file:

    - task: PublishPipelineArtifact@1
      inputs:
          targetPath: $(System.DefaultWorkingDirectory)/dist
          artifactName: replayer-pwa-distribution

This now creates an artifact, with each pipeline run, related to the run itself. Note: it’s not published in the “Artifacts” for the project, but in the published artifacts for the run:

A published artifact for a pipeline run
A published artifact for a pipeline run

Skipping unnecessary files

I build the stats.json file for analyzation with the webpack-bundle-analyzer. While useful for observing dependencies and build size, it’s of no use in the distribution. With an .artifactignore file I am skipping over it for the artifact:

# An ignore file for the Azure pipeline artifact
# Statistics are not used in the distribution
stats.json

This artifact can now get deployed to the web space, either manually or with a release pipeline.

Deploying to the web space

Typically you use a separate release pipeline for releasing build artifacts. However, it’s also possible to directly deploy build output from the build pipeline. For simplicity, this is how I deploy my build here, into my “devel stage” test environment only.

Azure Pipelines offers two options for deploying to an external (non-Azure) server:

  • SSH-based access, with both an SSH key or a credential based option.
  • FTP/SFTP based file transfers

Because my hosting provider‘s SSH option just gives global access, I chose to use the FTP Upload task, with a specific user for the path I want to deploy into. This is the (example) task I added to the end of my azure-pipelines.yml file:

    # FTP upload the build output
    # Note: This does not honor the .artifactignore file
    - task: FtpUpload@2
      inputs:
          credentialsOption: 'inputs' 
          #The FTP server URL must begin with ftp:// or ftps://
          serverUrl: 'ftps://example.cyon.net' 
          username: 'example-user@example.cyon.net'. 
          password: 'super-long-and-secure-password' 
          rootDirectory: $(System.DefaultWorkingDirectory)/dist
          # Deploy all except the stats.json
          filePatterns: '**/!(stats.json)'
          #Just use the defined root for the FTP user
          remoteDirectory: '/'
          #keep the directory, as it's the root anyway
          clean: false
          #removing content does not work, because I have static files that are not allowed to access for this upload task
          cleanContents: false
          preservePaths: true

Note that this does not honor the .artifactignore file. Thus I specify the minimatch file matching pattern to exclude the stats.json file likewise.

Also, you should not expose the password in the azure-pipelines.yml directly (like in this example), but use secret variables.

Summary

Azure Pipelines is a straightforward and free solution for a CI/CD (for public projects). After initial setup of the organization and project, the build pipeline is completely configurable with a single YAML file. Here’s my full azure-pipelines.yml file, incorporating all steps from above:

# Build Pipeline for replayer-pwa

trigger:
    - main

pool:
    vmImage: ubuntu-latest

steps:
    - task: NodeTool@0
      inputs:
          versionSpec: '10.x'
      displayName: 'Install Node.js'
    # Building the package
    - script: |
          npm install
          npm test 
          npm run build
      displayName: 'npm install and build'
    # Publishing the test results
    - task: PublishTestResults@2
      condition: succeededOrFailed()
      inputs:
          testRunner: JUnit
          testResultsFiles: '**/junit.xml'
    # Publishing the build pipeline artifact (optional)
    # This honors the .artifactignore file
    - task: PublishPipelineArtifact@1
      inputs:
          targetPath: $(System.DefaultWorkingDirectory)/dist
          artifactName: replayer-pwa-distribution
    # FTP upload the build output
    # Note: This does not honor the .artifactignore file
    - task: FtpUpload@2
      inputs:
          credentialsOption: 'inputs' # Options: serviceEndpoint, inputs
          #The FTP server URL must begin with ftp:// or ftps://
          serverUrl: 'ftps://example.cyon.net' 
          username: 'example-user@example.cyon.net'. 
          password: 'super-long-and-secure-password' 
          rootDirectory: $(System.DefaultWorkingDirectory)/dist
          # Deploy all except the stats.json
          filePatterns: '**/!(stats.json)'
          #Just use the defined root for the FTP user
          remoteDirectory: '/'
          #keep the directory, as it's the root anyway
          clean: false
          #removing content does not work, because I have static files that are not allowed to access for this upload task
          cleanContents: false
          preservePaths: true

Read the Build, test, and deploy JavaScript and Node.js apps guide for more details.

For reference, you can access the public pipeline runs for my example project above: https://dev.azure.com/suterma/replayer-pwa/_build?definitionId=1