Angular 7 with Azure DevOps Build Pipeline
Angular 7 is a very useful JavaScript framework for building applications. Being able to build these projects on Azure DevOps Pipelines is very useful.
- Updated January 27th 2019 with Linting and E2E tests.
- January 2nd, 2020: Updated with package.json script additions to make puppeteer dowloaded browser used..
- January 2nd, 2020: Updated to Angular 8 in this blog post here..
Angular Project
I’ve created a new repository on GitHub here.
Following the Angular Quickstart here I created a new folder and put Angular 7 project in there.
> mkdir src
> cd src
> npm install -g @angular/cli
> ng new angular7
Azure DevOps Pipeline definition
I tend to like storing my build definitions inside my repository with my code, so I’ve added an “azure-pipelines.yml” to the root of the repo with the following definition.
variables:
buildConfiguration: 'Release'
steps:
- task: Npm@1
displayName: 'npm install'
inputs:
command: install
workingDir: src/angular7
- task: Npm@1
displayName: 'Build Angular'
inputs:
command: custom
customCommand: run build -- --prod
workingDir: src/angular7
- task: PublishPipelineArtifact@0
inputs:
artifactName: 'angular'
targetPath: 'src/angular7/dist'
In three steps, I can install NPM packages, build the Angular project, and then publish it as a build artifact.
One thing to call out on the build command is that anything after the --
is passed as an argument to whatever is contained in the build
command. So the definition in package.json for "build": "ng build",
becomes ng build --prod
.
Pipeline
Even though the repository is stored in GitHub, I can still select it on Azure Build Pipelines.
Azure Pipeline build definition will get automatically populated from the definition.
Click Run
button on the top right. The pipeline will automatically be triggered anytime code happens to be committed to GitHub.
The pipeline artifacts will be the Angular application.
This is a good start but there’s some quality of life things that would be nice to add, such as tests and code-coverage.
Code-Coverage
Code coverage is a pretty neat thing to know about the application. Angular can generate a code-coverage report using this command: ng test --watch=false --code-coverage
. Azure DevOps only has these two formats to accept for Code Coverage results.
I’m going to add Cobertura output to karma.conf.js.
module.exports = function (config) {
...
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
fixWebpackSourcePaths: true
},
...
};
Running Angular tests spits out coverage files.
Adding Puppeteer to dependencies makes it easier to run headless Chrome, especially with Angular on Azure DevOps.
> npm install puppeteer --save-dev
To use the version of chromium that puppeteer downloads can be done by this package.json scripts addition.
{
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"install-puppeteer": "cd node_modules/puppeteer && npm run install",
"test": "npm run install-puppeteer && ng test",
"lint": "ng lint",
"e2e": "npm run install-puppeteer && ng e2e"
}
}
Editing the karma.conf.js makes it easier to run the tests. Do note the use of puppeteer’s downloaded package. That makes it easier to standardize browser version across both the build server and local development.
module.exports = function (config) {
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
singleRun: false
});
};
Further, adding the steps to the build pipeline to run this automatically.
- task: Npm@1
displayName: 'Test Angular'
inputs:
command: custom
customCommand: run test -- --watch=false --code-coverage
workingDir: src/angular7
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage Angular results'
condition: succeededOrFailed()
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: 'src/angular7/coverage/cobertura-coverage.xml'
reportDirectory: src/angular7/coverage
failIfCoverageEmpty: true
An interesting thing to note, is that I want to see code coverage results regardless of if a previous test failed:
condition: succeededOrFailed()
Tests
Next, lets dig into adding tests output. I’m going to stick with tests from ng test
, basically the tests being run above.
My options for publishing test results are JUnit, NUnit, XUnit, or VSTest
Let’s add the karma-junit-reporter.
> npm install karma-junit-reporter --save-dev
Further add it to the karma.conf.js configuration.
module.exports = function (config) {
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
require('karma-junit-reporter')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml', 'junit'],
junitReporter: {
outputDir: '../junit'
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
singleRun: false
});
};
Now let’s add Angular test results to the build pipeline. Note the same condition as previous code coverage, I want to see test results if succeeded, and especially if failed.
- task: PublishTestResults@2
displayName: 'Publish Angular test results'
condition: succeededOrFailed()
inputs:
searchFolder: $(System.DefaultWorkingDirectory)/src/angular7/junit
testRunTitle: Angular
testResultsFormat: JUnit
testResultsFiles: "**/TESTS*.xml"
Add this as a beginning step, so leftover resutls don’t get pulled in from previous builds.
- task: DeleteFiles@1
displayName: 'Delete JUnit files'
inputs:
SourceFolder: src/angular7/junit
Contents: 'TESTS*.xml'
Next time the build runs, you can see test results uploaded
Linting
The next piece of the puzzle is to add linting to the results. In reality, this is just static analysis to make sure that the code follows guidelines. An example being to have semi-colons at the end of lines as shown in this image.
Adding lint output to the Azure DevOps build is as simple as adding this task to the end.
- task: Npm@1
displayName: 'Lint Angular'
inputs:
command: custom
customCommand: run lint -- --format=stylish
workingDir: src/angular7
E2E (end-to-end) tests
There’s one more level of tests that can be run, end-to-end tests. Using protractor, Angular runs e2e tests which are application-level tests. The previous tests shown in this pipeline are unit tests in isolation for individual components, directives, and other Angular pieces. These e2e tests run the entire application.
The process here is very similar to the tests from above, I’m going to edit the protractor.conf.js file to use both Junit and puppeteer and headless chrome. I already have puppeteer in the package.json from above, so do add the jasmine reporters package.
> npm install jasmine-reporters --save-dev
protractor.conf.js:
const { SpecReporter } = require('jasmine-spec-reporter');
const { JUnitXmlReporter } = require('jasmine-reporters');
process.env.CHROME_BIN = process.env.CHROME_BIN || require("puppeteer").executablePath();
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome',
chromeOptions: {
args: ["--headless", "--disable-gpu", "--window-size=1200,900"],
binary: process.env.CHROME_BIN
}
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () { }
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
var junitReporter = new JUnitXmlReporter({
savePath: require('path').join(__dirname, './junit'),
consolidateAll: true
});
jasmine.getEnv().addReporter(junitReporter);
}
};
Adding the e2e tests to the pipeline look extremely similar to the tests from above, but without the code coverage upload.
- task: Npm@1
displayName: 'E2E Test Angular'
inputs:
command: custom
customCommand: run e2e
workingDir: src/angular7
- task: PublishTestResults@2
displayName: 'Publish Angular E2E test results'
condition: succeededOrFailed()
inputs:
searchFolder: $(System.DefaultWorkingDirectory)/src/angular7/e2e/junit
testRunTitle: Angular_E2E
testResultsFormat: JUnit
testResultsFiles: "**/junit*.xml"
I can then go into Azure DevOps and see the E2E test results too.
Build Logs
The logs for the build pipeline show the steps clearly, and you may inspect each one closer as convenient.
Build Summary
The build summary shows the high level view that I have 100% code coverage and 100% succeeding tests. If tests fail, or code coverage drops, that’ll be shown here too.
Code Changes
Here’s the overview of code changes.
Added Build Pipeline YAML definition:
variables:
buildConfiguration: 'Release'
steps:
- task: DeleteFiles@1
displayName: 'Delete JUnit files'
inputs:
SourceFolder: src/angular7/junit
Contents: 'TEST*.xml'
- task: Npm@1
displayName: 'npm install'
inputs:
command: install
workingDir: src/angular7
- task: Npm@1
displayName: 'Build Angular'
inputs:
command: custom
customCommand: run build -- --prod
workingDir: src/angular7
- task: PublishPipelineArtifact@0
inputs:
artifactName: 'angular'
targetPath: 'src/angular7/dist'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'src/angular7/dist'
ArtifactName: angular2
- task: Npm@1
displayName: 'Test Angular'
inputs:
command: custom
customCommand: run test -- --watch=false --code-coverage
workingDir: src/angular7
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage Angular results'
condition: succeededOrFailed()
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: 'src/angular7/coverage/cobertura-coverage.xml'
reportDirectory: src/angular7/coverage
failIfCoverageEmpty: true
- task: PublishTestResults@2
displayName: 'Publish Angular test results'
condition: succeededOrFailed()
inputs:
searchFolder: $(System.DefaultWorkingDirectory)/src/angular7/junit
testRunTitle: Angular
testResultsFormat: JUnit
testResultsFiles: "**/TESTS*.xml"
- task: Npm@1
displayName: 'Lint Angular'
inputs:
command: custom
customCommand: run lint -- --format=stylish
workingDir: src/angular7
- task: Npm@1
displayName: 'E2E Test Angular'
inputs:
command: custom
customCommand: run e2e
workingDir: src/angular7
- task: PublishTestResults@2
displayName: 'Publish Angular E2E test results'
condition: succeededOrFailed()
inputs:
searchFolder: $(System.DefaultWorkingDirectory)/src/angular7/e2e/junit
testRunTitle: Angular_E2E
testResultsFormat: JUnit
testResultsFiles: "**/junit*.xml"
package.json additions:
{
"devDependencies": {
"karma-junit-reporter": "^1.2.0",
"puppeteer": "^1.11.0",
"jasmine-reporters": "^2.3.2",
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"install-puppeteer": "cd node_modules/puppeteer && npm run install",
"test": "npm run install-puppeteer && ng test",
"lint": "ng lint",
"e2e": "npm run install-puppeteer && ng e2e"
}
}
Final karma.conf.js:
module.exports = function (config) {
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
require('karma-junit-reporter')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml', 'junit'],
junitReporter: {
outputDir: '../junit'
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
singleRun: false
});
};
Final protractor.conf.js:
const { SpecReporter } = require('jasmine-spec-reporter');
const { JUnitXmlReporter } = require('jasmine-reporters');
process.env.CHROME_BIN = process.env.CHROME_BIN || require("puppeteer").executablePath();
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome',
chromeOptions: {
args: ["--headless", "--disable-gpu", "--window-size=1200,900"],
binary: process.env.CHROME_BIN
}
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () { }
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
var junitReporter = new JUnitXmlReporter({
savePath: require('path').join(__dirname, './junit'),
consolidateAll: true
});
jasmine.getEnv().addReporter(junitReporter);
}
};
Summary
My code is here. Azure DevOps makes builds easy.