Introduction
Azure DevOps is a Microsoft product that, alongside other features such as Source Code Management and Project Management, allows your team to set up Continuous Integration for their project(s).
Continuous Integration is a development practice that enables your team to improve quality, and deliver more stable software, benefiting both the team, and the end-user.
With continuous integration, several checks (referred to as a pipeline) are automatically run on certain triggers, such as a code change. These checks typically ensure that the code can be compiled successfully, and that all tests are passing. Automatically running these checks, in an isolated environment, takes away most of the issues that do work on my machine, and ensures our code-base is in good health at all times.
Angular application
For this demo, we'll be using a default Angular CLI application for which we'll be adding a Build pipeline on Azure DevOps.
Ensure you have the latest version of @angular/cli
by running npm install --global @angular/cli
, and create a new angular application using ng new ng-azure-devops --skipInstall=true
; we don't need Angular routing, and you can pick the stylesheet format of your choice.
Creating the Azure DevOps project
If you don't have a project on Azure DevOps yet, head over to https://dev.azure.com, and create a new one for this demo, provide a name, and (optionally) a description. You can choose which visibility you want. I'm going with public, but private would work just fine.
Once the project is created, you'll be taken to the project's dashboard.
Adding the code to Azure DevOps
On your project's sidebar, you should find Repos. Clicking it will take you to the default Git repository that was created for this project. You can add more repositories in your project if needed, so you don't need to create multiple projects if your application is spread over multiple Git repositories. For this article, we only need a single repository, so we're going to use the one that was created by default.
The repository's home should tell you it's empty, and provide you with instructions to push your code to the repository.
You'll need to configure your SSH key or use the Git credential manager in order to be able to commit to a repository on Azure DevOps.
Once authentication is configured, commit, and push the Angular project that we've created using the Angular CLI.
If you want to use GitHub, you can add your code to Github, and connect your Github account in Azure DevOps by going to the Project Settings > Github Connections (it's categorized under Boards in the Project Settings). You'll need to select the GitHub organization, as well as the repositories you want to integrate into the current project.
The next steps will work identical for both GitHub repositories and Azure DevOps repositories.
Creating a build pipeline
Now that we have our code either in an Azure DevOps repository or on GitHub (with our GitHub account connected through Project Settings), we can create a build pipeline for that repository by navigating to Pipelines from the project's sidebar.
Click on New Pipeline, select Azure Repos Git or GitHub (or anything else if you're using any of the other available options), click the corresponding repository (if you selected GitHub, you'll be prompted to authorize Azure Pipelines) and select Node.js with Angular as the pipeline configuration. This will preconfigure the pipeline to run npm install
, and ng build —-prod
.
In the review step, we'll see an example of the yaml file that's being generated by Azure DevOps.
# Node.js with Angular
# Build a Node.js project that uses Angular.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install -g @angular/cli
npm install
ng build --prod
displayName: 'npm install and build'
The default configuration will trigger for each change on the master branch. It'll use an ubuntu image, and executes two steps:
- Install Node.js
- Install dependencies and build the application.
Clicking Save And Run will add the yaml file your repository, keeping your build pipeline configuration together with the source code. We can even make changes to the yaml file locally, and commit them, Azure DevOps will pick up these changes, and update your build pipeline accordingly.
Creating a separate branch, and starting a pull request allows you to not merge this configuration in master before it's completely configured in the way that you want. To simplify things, we'll be committing changes directly to master.
If you prefer to use a separate branch, you can still do so. However, do know that builds are not triggered automatically as long as there's no azure-pipelines.yml file on master branch. This means you'll need to manually trigger builds until your configuration is merged into master.
In case of creating a pipeline, clicking Save and Run will run it either way, but as we'll be making changes to the source code, builds will not be triggered automatically.
Once the configuration file has been saved, a new build is triggered, and after a while, all steps should have been completed successfully.
Some of the above steps are added by Azure DevOps (Prepare, Initialize, Checkout, Post Checkout and Finalize), while others are defined as part of our yaml configuration (Install Node.js and npm install and build). If you want to get more information for a specific step, clicking it will show all the output for that particular step:
Finetuning the build pipeline
We haven't made any changes to the default Node.js with Angular configuration yet. Before adding additional steps, let's change the configuration in such a way that installing the dependencies, and building the project are two separate steps: remove the global installation of @angular/cli
and make use of npx instead.
To edit the build pipeline, click pipelines from the project's sidebar, select the corresponding build pipeline, and click edit on the upper right corner:
When clicking edit, Azure DevOps will open an editor for the yaml file that was created earlier. Edit the contents of the file so that it's identical to the yaml configuration below:
# Node.js with Angular
# Build a Node.js project that uses Angular.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Setup Environment'
- script: npm install
displayName: 'Install Dependencies'
- script: npx ng build --prod
displayName: 'Build'
Saving the configuration should trigger a new build. You should see the Install Dependencies, and Build steps being two individual steps.
Azure DevOps is now integrated to run ng build
on every commit to master. Apart from building the project, we might also want to run all of the unit tests so that we know that all of the code is still working as expected.
Running Unit Tests on Azure DevOps
Before we'll be able to run the unit tests on Azure DevOps, we'll need to make some changes to the project that was generated using Angular CLI.
As the host, on which the pipeline is executing all of the automated steps, has no Chrome installed, we'll need to add puppeteer to be able to use ChromeHeadless on Azure DevOps.
npm install puppeteer --save-dev
Open karma.conf.js, and add the following to the top:
const process = require('process');
process.env.CHROME_BIN = require('puppeteer').executablePath();
This will load puppeteer, and use its executablePath for the CHROME_BIN environment variable, allowing for karma to use puppeteer when running Chrome or ChromeHeadless.
As a last step, update the karma.conf.js file to use the ChromeHeadless browser.
{
//...
browsers: ['ChromeHeadless']
//...
}
Updating karma.conf.js to use puppeteer will use puppeteer on your local machine as well. If this is not something you want, you can create a copy of the karma.conf.js file, name it karma.conf.ci.js (or anything you like), and pass it to the
ng test --karmaConfig karma.conf.ci.js
command when running it in the pipeline (we'll be configuring this later).
Commit and push these changes to the repository. This will trigger a build, but this won't execute the tests yet as we first have to update the build pipeline to run the tests. Add a new step to the configuration in Azure DevOps, either before or after the Build step.
- script: npx ng test --watch=false
displayName: 'Tests'
Once saved, the build should be triggered and completed successfully, including a Tests step. Clicking on the Tests step should show you the same information you would get when running ng test
locally.
Even though this gives us all the information we need regarding the tests, Azure DevOps has a built-in feature to show tests reports, enabling you to more easily navigate successful/failed tests.
Publishing test results to Azure DevOps
We'll need to publish the test results in a format that can be interpreted by Azure DevOps. In order to understand what formats are supported, lets head over to our build pipeline, and click edit.
On the right side of the build pipeline's edit screen, you should see a list of available tasks. Search for the task named Publish Test Results, and select it.
In the task details screen, you'll see a drop-down for the test result format. You can use any of the available formats, as long as your test runner allows you to export the results in the given format. As Karma supports JUnit, we'll be using that one.
Once added, the yaml configuration will now contain a new section for the PublishTestResults task:
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/TEST-*.xml'
Ensure this section was added after the step that's running npx ng test
, and make the following changes so that it has a displayName, and provide it with a condition so that it publishes test results even when the tests have failed (because we also want to be able to inspect failing tests).
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
# Make sure you've changed testResultsFiles to the one below
testResultsFiles: '**/TESTS-*.xml'
displayName: 'Publish Test Results'
As we haven't configured our Angular application to publish test results in the JUnit format yet, the build that's being triggered upon saving the above changes will not publish any test results yet.
Configuring Karma to publish JUnit
Install the appropriate reporter for JUnit in the Angular project by running npm install karma-junit-reporter --save-dev
.
Update karma.conf.js by including the karma-junit-reporter
in the plugins and adding junit
as one of the reporters.
module.exports = function (config) {
config.set({
...
plugins: [
...,
require('karma-junit-reporter')
],
...,
reporters: ['progress', 'kjhtml', 'junit'],
...
});
};
That's all we have to change in order to prepare our unit tests to be fully integrated into Azure DevOps. Commit and push the changes you made to the repository.
This should trigger a new build, once that's finished you should see the 3 default tests showing up in the Tests section on the build details (you might need to check the filters, by default it only shows failed tests).
By integrating our tests with Azure DevOps, we have a structured UI for tracking all of our unit tests. Whenever a test fails, we don't have to scroll through the console output on the tests step. Instead, we can inspect the tests altogether and filter to only show those we want.
If we'd have a failing test, we can even click on it to show the details of what went wrong, showing the same information we would get in the console:
Showing Code Coverage in a Build Pipeline
Azure DevOps provides us with the ability to show an overview of the code coverage of our application's unit tests. To do so, just like publishing the test results in a specific format, we'll need to publish the code coverage results in a supported format. Currently, Azure DevOps supports both Cobertura and JaCoCo. Istanbul, the code coverage tool used by the Angular CLI, has support for Cobertura built-in, so we don't need to add any dependency in order to use Cobertura.
We'll need to update the karma configuration to include Cobertura in the reports for the coverageIstanbulReporter:
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/ng-azure-devops'),
reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
fixWebpackSourcePaths: true
},
This will generate a file named cobertura-coverage.xml in the ./coverage/ng-azure-devops directory. We will be needing this path in the Build pipeline.
Commit, and push these changes to your repository (this will trigger a build that doesn't publish the code coverage yet), and head over to the build pipeline's configuration on Azure DevOps and add the following step:
- task: PublishCodeCoverageResults@1
condition: succeededOrFailed()
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/coverage/ng-azure-devops/cobertura-coverage.xml'
displayName: 'Publish Code Coverage Results'
Just like with publishing the test results, we want to include the code coverage results for failed tests as well. That's why we're setting the condition to succeededOrFailed()
. We also need to provide the location of the summary file (created by istanbul), and ensure this aligns with the location of the file, generated by istanbul.
In order to be able to publish the code coverage results, we'll need to run ng test
using the --code-coverage
flag. Update the corresponding step in the build pipeline configuration to include that flag:
- script: npx ng test --watch=false --codeCoverage=true
displayName: 'Tests'
Saving the configuration should run a build that, once completed, shows a Code Coverage section on the build's details page.
Publishing the code coverage results will also create a report and make it available as an artifact, attached to your build. If you ever wish to inspect the report, you can download it by opening the Artifacts menu on the upper right corner. Generally, you should find most information on the Code Coverage tab in Azure DevOps itself.
Adding Linting
We can ensure the build pipeline fails when code does not follow the coding guidelines by integrating our linter. Let's add a step to run npx ng lint
, either before or after the Build step.
- script: npx ng lint
displayName: 'Code Analysis'
This will add a basic integration with our linter. Azure DevOps doesn't have a built-in way to keep track of code quality. However, it does have tasks available to integrate 3th party tools such as SonarQube if you would want to integrate with it.
Publishing Artifacts
Now that we have an automated way to verify that the source code compiles, all of the unit tests are running successfully, and code matches with our coding guidelines, we can publish the output of ng build
so that we can use it to deploy, either manual or automated.
Add the following step to the end of the Build Pipeline configuration
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'dist/ng-azure-devops'
ArtifactName: 'web-app'
publishLocation: 'Container'
displayName: 'Publish Artifacts'
In order to publish the artifacts, we need to provide the PathtoPublish, which is the output folder of the ng build
command, an artifact name, and the publishLocation. Providing "Container" as the publishLocation means that it will publish the artifacts as part of the Azure Pipeline itself.
Save the changes, and wait for the build to complete. Once completed, you can download the artifacts from the build result's page:
Every time a build is successful, the latest version will be published as an artifact. This allows us to either download it, or use it in an automated way to deploy our application.
Conclusion
With Azure DevOps, we can easily integrate our Angular application in such a way that we can ensure our codebase is in good health at all times. This should reduce the risk of releasing bugs, resulting in more stable software, and both a happier team, and end-user.
We're also creating our build artifacts in an automated way, reducing time, and complexity, to deploy our application.
In a future article, we'll set up a release pipeline to automatically deploy the build artifacts.
This article was written by Frederik Prijck who is a Software Engineer at This Dot.
You can follow him on Twitter at @frederikprijck.
Need JavaScript consulting, mentoring, or training help? Check out our list of services at This Dot Labs.
Top comments (6)
Almost a perfect tutorial : )
Good tutorial thanks!
You should use npm script instead of npx commands to make sure you use the version written in the package.json.
Thanks for the feedback.
Npx uses the local version (defined by package.json) by default. Only if it can't find the package locally, it will fall back to the global version. See: prntscr.com/r404st
I think using npm scripts is definitely a good idea. But npx is also fine in terms of ensuring it uses the correct version as defined in the package.json.
Ok, Good to know, thanks.
Hi Frederik, this is a great article, but I'm having a lot of struggles while running npx ng test --watch=false, I'm getting following errors in DevOps:
HeadlessChrome 80.0.3987 (Linux 0.0.0): Executed 0 of 0 SUCCESS (0 secs / 0 secs)
HeadlessChrome 80.0.3987 (Linux 0.0.0): Executed 0 of 0 ERROR (0.021 secs / 0 secs)
[error]Bash exited with code '1'.
I'm new to angular and testing, and this has been a huge struggle for me, do you know how to solve this problem?
hi fredrick thanks for the article how do you go about publishing on different environments , stage , production etc
thanks