Build a CI/CD Pipeline with Azure DevOps
Build a YAML pipeline that auto-builds and tests a Node.js app on every push.
Introduction
30 Second Summary
Imagine pushing code to your team's shared repository and having it automatically tested and packaged before anyone else can use it. That's what a CI/CD pipeline does for software teams around the world.
In this project, you will build a CI/CD pipeline in Azure DevOps that automatically builds, tests, and packages a Node.js app every time you push code. You'll even break a test on purpose to watch the pipeline catch the problem.
What You'll Build
You will create a Node.js calculator app with unit tests and connect it to a multi-stage Azure DevOps YAML pipeline that triggers automatically on every push to main.
By the end of this project, you'll have:
- A Node.js calculator app with unit tests using the built-in Node.js test runner.
- A YAML pipeline in Azure DevOps with Build and Package stages that runs on every push.
- A published build artifact you can download from a successful pipeline run.
- Secret Mission: Set up PR validation with branch policies so tests must pass before code can merge.
Are there any prerequisites?
You should have basic familiarity with Git (commit, push, clone). No prior DevOps or testing experience is required.
Before We Start
Before We Start...
Set Up Your Azure DevOps Project
Before writing any code, you need two foundations: a cloud platform to run your pipeline and a local Git repository to hold your code.
Azure DevOps is Microsoft's platform for CI/CD pipelines. Once your project and Git repository are in place, you'll be ready to start writing code in the next step.
In this step, get ready to:
- Create a new Azure DevOps project.
- Verify your account has parallel jobs to run pipelines.
- Initialize a local Git repository for your code.
Create a new project in Azure DevOps
An Azure DevOps project is your workspace for managing code, running pipelines, and tracking builds. You'll create one now and use it for the rest of this project.
- Go to https://dev.azure.com in your browser.
- Sign in with your Microsoft account.
- Click New project in the top-right corner of the page.
- Enter calculator-pipeline as the project name.
- Under Visibility, select Private.
- Click Create.
You should see your new calculator-pipeline project dashboard with sections like Boards, Repos, and Pipelines in the left sidebar.
Why Private visibility?
Private projects are only visible to people you explicitly invite. This keeps your pipeline and code hidden from the public while you learn.
Can't sign in?
You need a Microsoft account to use Azure DevOps. If you don't have one, create a free account at account.microsoft.com first.
If you're signed in but don't see your organization, Azure DevOps will prompt you to create one on your first visit. Follow the prompts to set it up.
Help me sign in to Azure DevOps
Verify your parallel jobs
Your pipeline needs a parallel job to run. A parallel job is the compute slot that Azure DevOps uses to execute each pipeline run.
- Click the gear icon in the bottom-left corner of the page to open Project Settings.
- In the left sidebar, scroll down to the Pipelines section.
- Click Parallel jobs.
Look at the Microsoft-hosted row. How many parallel jobs do you see?
✔️ I see at least 1 parallel job
You're all set. Your account has the compute resources needed to run pipelines.
ⓧ I see 0 parallel jobs
New Azure DevOps organizations sometimes start with 0 free parallel jobs. You need to request a free grant from Microsoft.
- Go to https://aka.ms/azpipelines-parallelism-request.
- Fill in the form with your Azure DevOps organization name and email.
- Submit the request.
This can take a few business days
Microsoft reviews these requests manually, so approval can take up to a few business days. You can continue setting up your project locally while you wait, but you won't be able to run the pipeline until the grant is approved.
What are parallel jobs?
A parallel job is a compute slot that Azure DevOps allocates to run your pipeline. The free tier gives you 1 Microsoft-hosted parallel job with 1,800 minutes (30 hours) per month. That's more than enough for this project.
Initialize a Git repository for your project
Your Azure DevOps project is ready in the cloud. Now you need a local folder on your computer where you'll write code and push it up to Azure DevOps later.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and initialize a Git repository by running these commands:
mkdir calculator-pipeline
cd calculator-pipeline
git init
What do these commands do?
- mkdir calculator-pipeline creates a new folder called calculator-pipeline on your Desktop.
- cd calculator-pipeline moves your terminal into that new folder.
- git init turns the folder into a Git repository so you can track changes and push code to Azure DevOps.
- Confirm Git is initialized by running:
git status
You should see output that includes On branch main (or On branch master) and No commits yet.
Seeing 'git: command not found'?
This means Git is not installed on your machine. Visit git-scm.com/downloads to download and install it. After installing, close and reopen your terminal before trying again.
Help me install Git on my computer
Your Azure DevOps project is set up and your local Git repository is ready. Next up, you'll build the Node.js calculator app and write unit tests for it.
Build the Node.js App and Write Tests
Your Azure DevOps project is created and your local Git repository is initialized. Now it's time to write the code that your pipeline will build and test.
Every CI/CD pipeline needs something to build. You'll create a small calculator module with three math functions, then write automated tests to verify they work correctly.
In this step, get ready to:
- Initialize a Node.js project and create a calculator module.
- Write automated tests using the built-in Node.js test runner.
- Run the tests locally and confirm they all pass.
Initialize the Node.js project and create the calculator module
Before writing any code, you need a package.json file to define your Node.js project. This file tracks your project's name, version, and scripts.
- Make sure your terminal is still inside the calculator-pipeline folder (the one you created in the previous step).
- Generate a default package.json by running this command:
npm init -y
- Confirm the file was created by running:
ls
You should see package.json listed in the output.
npm: command not found?
This means Node.js is not installed or not in your system path. Run node --version to check. If it also fails, install Node.js from the official site before continuing.
Help me troubleshoot why npm is not found on my machine.
Now you'll create the calculator module. This is the actual application code your pipeline will build and test later.
- Create a new file called calculator.js in your calculator-pipeline folder and add the following code:
// Add two numbers together
function add(a, b) {
return a + b;
}
// Subtract the second number from the first
function subtract(a, b) {
return a - b;
}
// Multiply two numbers together
function multiply(a, b) {
return a * b;
}
// Export all functions so other files can use them
module.exports = { add, subtract, multiply };
What does this code do?
This module defines three simple math functions: add, subtract, and multiply. Each takes two numbers and returns the result.
The module.exports line makes these functions available to other files. Your test file will import them from here.
- Save calculator.js.
- Confirm the file exists by running ls in your terminal. You should see both calculator.js and package.json listed.
Don't see calculator.js?
Make sure you saved the file inside the calculator-pipeline folder, not a parent or sibling folder. Check that the filename is exactly calculator.js with no extra spaces or capital letters.
Help me figure out why my calculator.js file isn't showing up in my project folder.
✔️ Awesome, I've got everything!
Great. Double check you've saved calculator.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete calculator.js file. Make sure yours matches:
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, subtract, multiply };
Write tests for the calculator
Your calculator module needs automated tests to prove it works. Tests are the core of any CI/CD pipeline.
Node.js has a built-in test runner called node:test. This means zero external dependencies to install. It uses the same describe/it pattern you'll see in professional testing frameworks.
- Create a new file called calculator.test.js in your calculator-pipeline folder and add the following code:
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { add, subtract, multiply } = require('./calculator');
// Group all calculator tests together
describe('Calculator', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('should subtract two numbers', () => {
assert.strictEqual(subtract(10, 4), 6);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 7), 21);
});
// Edge case: negative inputs
it('should handle negative numbers', () => {
assert.strictEqual(add(-1, -2), -3);
});
// Edge case: multiplying by zero
it('should handle zero', () => {
assert.strictEqual(multiply(5, 0), 0);
});
});
What does this test file do?
The first three lines import everything the tests need. describe and it come from Node.js's built-in test runner. assert comes from Node.js's built-in assertion module. The three calculator functions come from your calculator.js file.
Each it() block is one test case. assert.strictEqual() checks that the actual result matches the expected value. If any assertion fails, the test fails.
The last two tests cover edge cases like negative numbers and zero. These are the kinds of checks that catch subtle bugs before they reach production.
- Save calculator.test.js.
- Run ls in your terminal. You should now see calculator.js, calculator.test.js, and package.json listed.
Don't see calculator.test.js?
Check that you saved the file inside the calculator-pipeline folder. The filename must end in .test.js (not .test.txt or .tests.js). The Node.js test runner auto-discovers files matching the *.test.js pattern, so the name matters.
Help me figure out why my test file isn't appearing in the right folder.
✔️ Awesome, I've got everything!
Great. Double check you've saved calculator.test.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete calculator.test.js file. Make sure yours matches:
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { add, subtract, multiply } = require('./calculator');
describe('Calculator', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('should subtract two numbers', () => {
assert.strictEqual(subtract(10, 4), 6);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 7), 21);
});
it('should handle negative numbers', () => {
assert.strictEqual(add(-1, -2), -3);
});
it('should handle zero', () => {
assert.strictEqual(multiply(5, 0), 0);
});
});
Run the tests locally
Your code and tests are written. Before your pipeline can run them automatically, you need to tell Node.js how to find your tests.
The package.json file that npm init -y generated has a placeholder test script. You need to replace it with the real command.
- Open package.json in your text editor.
- Replace the entire contents with the following:
{
"name": "calculator-pipeline",
"version": "1.0.0",
"description": "A simple calculator app for learning Azure DevOps CI/CD pipelines",
"main": "calculator.js",
"scripts": {
"test": "node --test"
},
"keywords": [],
"author": "",
"license": "ISC"
}
What changed from the default?
- The "test" script now runs node --test instead of the placeholder error message. This tells Node.js to auto-discover and run any file matching the *.test.js pattern.
- The "main" field now points to calculator.js instead of the default index.js.
- The "description" field has a short summary of what this project is for.
- Save package.json.
✔️ Awesome, I've got everything!
Great. Double check you've saved package.json before moving on.
ⓧ I'd like to double check the full code
Here is the complete package.json file. Make sure yours matches exactly:
{
"name": "calculator-pipeline",
"version": "1.0.0",
"description": "A simple calculator app for learning Azure DevOps CI/CD pipelines",
"main": "calculator.js",
"scripts": {
"test": "node --test"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Now run the tests to make sure everything works. This is the same sequence your pipeline will follow automatically later.
- Install project dependencies and run the test suite by running these commands:
npm install
npm test
Why npm install with no dependencies?
This project uses only built-in Node.js modules, so there are no external packages to download. Running npm install still sets up the node_modules folder and generates a package-lock.json file.
Including this step mirrors the pipeline's flow. Your pipeline will always run npm install before npm test because real projects have dependencies.
You should see output showing all 5 tests passing. Look for green checkmarks or ok next to each test name, and a summary line at the bottom confirming 5 tests passed with 0 failures.
Seeing test failures or errors?
- If you see Error: Cannot find module './calculator', make sure calculator.js and calculator.test.js are in the same folder.
- If you see ERR_MODULE_NOT_FOUND for node:test, your Node.js version may be too old. Run node --version and confirm it is v18 or higher.
- If you see echo "Error: no test specified", your package.json still has the default test script. Go back and update it so the test script reads node --test.
Help me debug why my npm test command is failing.
Your calculator app is built and tested. Next up, you'll push this code to Azure DevOps so your pipeline has something to work with.
Connect Your Code to Azure DevOps
Your calculator app has three functions and five passing tests. All that work lives on your local machine right now.
To build an automated pipeline, Azure DevOps needs access to your code. In this step, you'll push your project to Azure Repos, the Git hosting service built into Azure DevOps.
In this step, get ready to:
- Push your local code to Azure Repos.
- Verify your files appear in the Azure DevOps repository.
Push your code to Azure Repos
Azure Repos works the same way as GitHub. You push code to it, and Azure Pipelines can automatically build and test that code.
- In your Azure DevOps project, click Repos in the left sidebar.
The empty repository page displays clone URLs and push instructions.
- Under Clone to your computer, click the copy icon next to the HTTPS URL.
What URL am I copying?
This is your Azure Repos clone URL. It tells Git where your remote repository lives.
The URL follows this pattern: https://dev.azure.com/your-org/calculator-pipeline/_git/calculator-pipeline. Your organization name will be different.
- Back in your terminal, add the Azure Repos URL as your Git remote by running this command:
git remote add origin [[AZURE_REPO_URL="your Azure Repos HTTPS clone URL"]]
What does this command do?
git remote add origin tells your local Git repository where to push code. origin is the standard name for your main remote repository.
- Commit your files and push them to Azure Repos by running these commands.
- Git may open a browser window asking you to sign into Azure DevOps during the push. Sign in with the same account you used to create your project.
echo node_modules/ > .gitignore
git add .
git commit -m "Add calculator app with tests"
git branch -M main
git push -u origin main
What do these commands do?
- echo node_modules/ > .gitignore creates a file that tells Git to ignore the node_modules/ folder. You don't need to push installed packages because npm install recreates them.
- git add . stages all your project files for commit.
- git commit -m "..." saves a snapshot of your code with a descriptive message.
- git branch -M main ensures your branch is named main to match Azure Repos.
- git push -u origin main uploads your commit to Azure Repos.
- You should see output in your terminal ending with * [new branch] main -> main.
Push failed?
- If you see fatal: remote origin already exists, remove the old remote first by running git remote remove origin. Then try the git remote add command again.
- If authentication fails, make sure you're signed into the same Azure DevOps account in your browser.
- If you see error: src refspec main does not match any, make sure you ran git commit before git push. You need at least one commit before you can push.
- Why is my git push to Azure Repos failing?
Verify your code in Azure DevOps
- Go back to your Azure DevOps project in the browser.
- Click Repos in the left sidebar, then click Files.
- Confirm you can see calculator.js, calculator.test.js, package.json, and .gitignore listed in the repository.
The empty repository page from before is now replaced with your project files.
Don't see your files?
- Make sure your git push command completed without errors. Check your terminal for any error messages.
- Try refreshing the Azure DevOps page in your browser.
- Why aren't my files showing up in Azure Repos after I pushed?
Your code is live in Azure Repos. Next up, you'll create the YAML pipeline that automatically builds and tests this code every time you push.
Create the CI Pipeline
Your code is live in Azure Repos. Now it's time for the main event: creating the pipeline that automatically builds and tests your code every time you push.
The pipeline is defined in a single YAML file called azure-pipelines.yml. This file tells Azure DevOps exactly what to do with your code, stage by stage.
In this step, get ready to:
- Create the azure-pipelines.yml file with a Build stage that installs dependencies and runs tests.
- Add a Package stage that bundles your app into a downloadable build artifact.
- Push the pipeline file to Azure Repos.
Create the pipeline file with the Build stage
Every Azure DevOps pipeline starts with a azure-pipelines.yml file at the root of your repository. This file defines the trigger, the agent that runs your code, and the stages your pipeline executes.
- Create a new file called azure-pipelines.yml inside your calculator-pipeline folder and paste the following code:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: UseNode@1
inputs:
version: '22.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
What does this code do?
- trigger: - main tells Azure DevOps to run this pipeline every time code is pushed to the main branch.
- vmImage: 'ubuntu-latest' tells Azure DevOps to run the pipeline on a fresh Linux virtual machine hosted by Microsoft.
- The Build stage has three steps: install Node.js 22.x, run npm install to grab dependencies, and run npm test to verify the code works.
- Save azure-pipelines.yml.
- You won't see any output yet because the pipeline only runs after it's been pushed to Azure Repos. The next substep adds the Package stage before you push.
Add the Package stage
The Build stage verifies your code works. Now you'll add a second stage that packages your app into a downloadable build artifact.
Why two stages instead of one?
Splitting the pipeline into stages reflects how real CI/CD workflows operate. If the Build stage fails (a test breaks, for example), the Package stage never runs. Broken code is stopped before it goes any further.
- In azure-pipelines.yml, add the following code at the bottom of the file, directly below the Build stage:
- stage: Package
displayName: 'Package Artifact'
dependsOn: Build
jobs:
- job: PackageJob
displayName: 'Package and Publish'
steps:
- task: UseNode@1
inputs:
version: '22.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
calculator.js
calculator.test.js
package.json
node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy project files'
- task: PublishPipelineArtifact@1
inputs:
artifactName: 'calculator-app'
targetPath: '$(Build.ArtifactStagingDirectory)'
displayName: 'Publish pipeline artifact'
What does this code do?
- dependsOn: Build makes the Package stage wait for the Build stage to pass before it starts.
- CopyFiles@2 copies your project files (source code, tests, and installed packages) into a staging directory.
- PublishPipelineArtifact@1 publishes those files as a downloadable artifact named calculator-app.
- The Package stage reinstalls Node.js and dependencies because each stage runs on a fresh virtual machine. Nothing from the Build stage carries over.
- Save azure-pipelines.yml.
YAML not looking right?
YAML is very sensitive to indentation. The - stage: Package line must be indented at the same level as - stage: Build (two spaces). Make sure you are using spaces, not tabs.
help me fix YAML indentation in my azure-pipelines.yml file
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file. Make sure yours matches exactly.
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: UseNode@1
inputs:
version: '22.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
- stage: Package
displayName: 'Package Artifact'
dependsOn: Build
jobs:
- job: PackageJob
displayName: 'Package and Publish'
steps:
- task: UseNode@1
inputs:
version: '22.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
calculator.js
calculator.test.js
package.json
node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy project files'
- task: PublishPipelineArtifact@1
inputs:
artifactName: 'calculator-app'
targetPath: '$(Build.ArtifactStagingDirectory)'
displayName: 'Publish pipeline artifact'
Commit and push the pipeline file
Your pipeline file is complete. Push it to Azure Repos so Azure DevOps can detect it on the next run.
- Add, commit, and push azure-pipelines.yml by running these commands:
git add azure-pipelines.yml
git commit -m "Add CI/CD pipeline"
git push
- Go to your Azure DevOps project in your browser.
- Click Repos in the left sidebar, then click Files.
- Confirm you can see azure-pipelines.yml listed alongside calculator.js, calculator.test.js, and package.json.
Push failed or file not showing up?
- If git push asks for credentials, enter the same credentials you used when pushing in the previous step.
- If you see "nothing to commit", make sure you saved azure-pipelines.yml before running git add.
- If the file doesn't appear in Azure Repos, refresh the page and make sure you're on the main branch.
help me push my pipeline file to Azure Repos
Your pipeline is defined and pushed to Azure Repos. Next up, you'll run it in Azure DevOps and watch it build, test, and package your app automatically.
Run the Pipeline and Watch It Work
Your pipeline definition is in Azure Repos, but nothing is running yet. Azure Pipelines needs you to point it at your repository before it can start automating.
In this step, you'll create the pipeline in the Azure DevOps UI, watch both stages execute, and download the finished build artifact.
In this step, get ready to:
- Set up a new pipeline connected to your repository.
- Watch both the Build and Package stages complete successfully.
- Download the published build artifact.
Set up the pipeline in Azure DevOps
The azure-pipelines.yml file is already in your repo, but Azure Pipelines doesn't know it exists yet. You need to create a pipeline and point it at your repository.
- In your Azure DevOps project, click Pipelines in the left sidebar.
- Click New pipeline.
- Under "Where is your code?", select Azure Repos Git.
- Select the calculator-pipeline repository.
- Azure DevOps detects your existing azure-pipelines.yml file and shows you the YAML contents on the "Review your pipeline YAML" page.
- Click Run to start the pipeline.
What just happened?
Azure DevOps created a pipeline linked to your repository. Because your azure-pipelines.yml has a trigger on main, this pipeline will automatically run every time you push code to the main branch from now on.
Watch the pipeline execute
Your pipeline is now running on a fresh virtual machine in the cloud. You can watch every step happen in real time.
- Click on the running pipeline run to open the run details.
- You'll see two stages listed: Build and Test and Package Artifact.
- Click on the Build and Test stage to expand it.
- Click on the Install, Build, and Test job to see the individual step logs.
What should I see in the Build stage?
Each step completes in order. First, Node.js 22.x is installed. Then npm install runs to grab any dependencies. Finally, npm test runs your five calculator tests.
In the test output, you should see all 5 tests pass with a checkmark. This confirms your code works correctly on a clean machine, not just on your laptop.
- Once the Build and Test stage completes with a green checkmark, click on the Package Artifact stage.
- Click on the Package and Publish job to watch it copy files and publish the artifact.
Both stages should complete with green checkmarks. This means your code built, all tests passed, and the artifact was published successfully.
Why does the Package stage reinstall dependencies?
Each stage in a multi-stage pipeline runs on a completely fresh virtual machine. The Package stage has no access to anything the Build stage installed. It has to check out the code and run npm install again from scratch.
This is actually a feature, not a bug. A clean environment for every stage guarantees that your build is reproducible.
Pipeline stuck or failing?
- If the pipeline is queued but never starts running, your organization may not have parallel jobs available. Go to Project Settings > Pipelines > Parallel jobs and confirm you have at least 1 Microsoft-hosted parallel job.
- If the Build stage fails at "Run tests", double check that your calculator.test.js matches the code from the previous step exactly.
- If you see 0 parallel jobs, you need to request a free grant at https://aka.ms/azpipelines-parallelism-request. This can take a few business days to process.
Help me debug my Azure DevOps pipeline failure.
Download the build artifact
After both stages pass, Azure DevOps stores the packaged files as a downloadable artifact. This is the end product of your pipeline.
- Click the back arrow or navigate to Pipelines in the left sidebar to return to the pipeline runs list.
- Click on your completed pipeline run to open the run summary.
- Scroll down to the Related section on the run summary page.
- Click on 1 published to see the published artifacts.
- Click on calculator-app to browse the artifact contents.
- Click the download button (the down arrow icon) to download the artifact as a zip file.
The downloaded zip contains your calculator.js, calculator.test.js, package.json, and node_modules folder. This is the exact set of files your pipeline packaged.
Why package these files as an artifact?
In a real project, this artifact would be the deployable package. Other stages in your pipeline (like a Deploy stage) could pick up this exact artifact and push it to a server.
Packaging ensures you deploy the exact code that passed your tests. No surprises from a stale build or a missing file.
Don't see the artifact?
- Make sure both stages completed with green checkmarks. If the Build stage failed, the Package stage never runs, so no artifact gets published.
- Check that you're looking at the run summary page (not the pipeline overview). The Related section with the artifact link is on the individual run's summary.
Help me find my published artifact in Azure DevOps.
Your pipeline is fully working. It builds, tests, and packages your app automatically on every push. Next up, you'll intentionally break a test to see how the pipeline catches failures before they go any further.
Break a Test and Watch the Pipeline Catch It
In the last step, you ran your pipeline and watched it build, test, and package your app successfully.
The real test of a CI pipeline is whether it catches failures. In this step, you will deliberately break a test, push the broken code, and watch the pipeline stop the build automatically.
In this step, get ready to:
- Intentionally break a unit test and push it to Azure Repos.
- Watch the pipeline detect the failure and stop the build.
- Fix the test and experience the full CI feedback loop.
Intentionally break a test
To prove your pipeline catches broken code, you will change an expected test value so the test fails on purpose.
- Open calculator.test.js in your text editor.
- Find the first test inside the describe('Calculator') block. Look for this line:
assert.strictEqual(add(2, 3), 5);
- Replace 5 with 999, so the line now looks like this:
assert.strictEqual(add(2, 3), 999);
- Save calculator.test.js.
- Commit and push the broken test by running these commands:
git add calculator.test.js
git commit -m "Break test to demonstrate CI"
git push
- Go to your Azure DevOps project at https://dev.azure.com.
- Click Pipelines in the left sidebar.
- You should see a new pipeline run that is in progress or has already completed.
Pipeline not starting?
- Make sure you saved calculator.test.js before committing.
- Check that you ran all three commands: git add, git commit, and git push.
- Verify that your azure-pipelines.yml has trigger: - main set. The pipeline only triggers on pushes to the main branch.
Why isn't my Azure DevOps pipeline starting after I pushed?
Watch the pipeline fail
- Click on the latest pipeline run to open it.
- You should see two stages: Build and Test and Package Artifact.
- Click on the Build and Test stage to expand it.
- Click on the Install, Build, and Test job to see the individual steps.
The Run tests step should have a red icon indicating failure. Click on it to see the log output.
What should I see in the logs?
The test output shows that should add two numbers failed. The assertion error tells you the expected value was 999 but the actual result was 5.
This is exactly what your pipeline is designed to do. It ran the tests automatically, found the broken one, and stopped the entire build.
- Click Summary at the top of the run to return to the stage overview.
- Look at the Package Artifact stage. It should be grayed out or marked as skipped.
Why did the Package stage not run?
Your azure-pipelines.yml defines the Package stage with dependsOn: Build. This means Azure DevOps will only run Package if Build succeeds.
Since Build failed at the test step, the pipeline stopped and never attempted to package anything. Broken code never gets packaged or deployed.
Fix the test and push again
Now that you have seen the pipeline catch the failure, it is time to fix the broken test and complete the feedback loop.
- Open calculator.test.js in your text editor.
- Find the line you changed earlier:
assert.strictEqual(add(2, 3), 999);
- Replace 999 with 5, so the line is back to its correct value:
assert.strictEqual(add(2, 3), 5);
- Save calculator.test.js.
- Commit and push the fix by running these commands:
git add calculator.test.js
git commit -m "Fix broken test"
git push
- Go back to Pipelines in your Azure DevOps project.
- Click on the latest pipeline run.
- Watch both the Build and Test and Package Artifact stages complete with green checkmarks.
✔️ Awesome, I've got everything!
Great. Double check you have saved calculator.test.js with the correct expected value of 5.
ⓧ I'd like to double check the full code
Here is the full calculator.test.js file. Make sure your version matches exactly.
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { add, subtract, multiply } = require('./calculator');
describe('Calculator', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('should subtract two numbers', () => {
assert.strictEqual(subtract(10, 4), 6);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 7), 21);
});
it('should handle negative numbers', () => {
assert.strictEqual(add(-1, -2), -3);
});
it('should handle zero', () => {
assert.strictEqual(multiply(5, 0), 0);
});
});
Pipeline still failing?
- Double check that you changed 999 back to 5 in calculator.test.js.
- Make sure you saved the file and pushed the change with all three commands: git add, git commit, and git push.
- Use the double check tab above to compare your full test file against the expected code.
Why is my Azure DevOps pipeline still failing after I fixed my test?
You have now experienced the complete CI feedback loop: push, fail, fix, push, pass. This is the cycle every development team relies on to catch bugs before they reach production.
Secret mission
Add PR Validation with Branch Policies
Your pipeline runs on every push to main. But in a real team, nobody pushes directly to main. In this secret mission, you'll set up branch policies so the pipeline must pass before any code can be merged.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your Azure DevOps project runs on the free tier, which includes 1,800 free pipeline minutes per month. The pipeline only consumes minutes when you push code, so there are no ongoing costs while it sits idle.
Resources you used:
- Azure DevOps project (calculator-pipeline).
- Azure Repos Git repository with your code.
- CI/CD pipeline defined in azure-pipelines.yml.
- Published build artifact (calculator-app).
- Local project folder (calculator-pipeline) on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- Your Azure DevOps project, pipeline, and repository stay exactly as they are.
- The pipeline only runs when you push code, so it will not consume any minutes while idle.
- Your local calculator-pipeline folder stays on your Desktop.
Pause - I'll come back to this later
Disable the pipeline so it stops running automatically. Your code and project stay intact for when you come back.
- In Azure DevOps, go to Pipelines.
- Click the three dots next to your pipeline and select Settings.
- Toggle the pipeline status to Paused. This prevents the pipeline from triggering on new pushes.
- Your code in Azure Repos and your local calculator-pipeline folder remain untouched.
- To resume later, return to the same settings and re-enable the pipeline.
Delete - I don't want to use this again
Remove all project resources from Azure DevOps and your local machine. This cannot be undone.
- In Azure DevOps, click Project Settings (gear icon, bottom left).
- Scroll down to Overview and click Delete at the bottom of the page.
- Type the project name calculator-pipeline to confirm, then click Delete. This removes the project, pipeline, repository, and all build artifacts.
- On your local machine, delete the calculator-pipeline folder from your Desktop.
Mission accomplished
That's a Wrap!
Nice work! You just built your first CI/CD pipeline from scratch using Azure DevOps. Every time you push code, your pipeline automatically builds, tests, and packages your app without you lifting a finger.
Here's what you learned:
- Built a Node.js app with unit tests using the built-in Node.js test runner, then created a YAML pipeline that automatically runs those tests on every push.
- Configured a multi-stage pipeline with Build and Package stages, then downloaded a published build artifact from a successful pipeline run.
- Intentionally broke a test and watched the pipeline catch the failure before the Package stage could run. Then fixed it and saw the full CI feedback loop in action: push, fail, fix, push, pass.
- Set up branch policies with PR validation so the pipeline blocks merges when tests fail.
Ready to quiz yourself?