Build a CI/CD Pipeline with Azure DevOps
Build a multi-stage YAML pipeline that auto-builds and tests a Node.js app.
Introduction
30 Second Summary
Every time a developer pushes code, they want to know immediately if something broke. Without automation, catching bugs means running tests manually, hoping nothing slipped through the cracks.
In this project, you will build a CI/CD pipeline in Azure DevOps that automatically builds and tests a Node.js application every time you push code to your repository.
What You'll Build
A fully automated CI/CD pipeline with a self-hosted build agent and a multi-stage YAML configuration that triggers on every code push.
By the end of this project, you'll have:
- An Azure DevOps organization and project with a Git repository storing your application code.
- A self-hosted Windows build agent running on your own machine, giving you unlimited free pipeline minutes.
- A multi-stage YAML pipeline with separate Build and Test stages that automatically runs on every commit.
- Secret Mission: Add a Deploy stage that packages your app as a downloadable pipeline artifact.
Are there any prerequisites?
You should have a basic understanding of Git (commit, push, clone) and be comfortable using a command line. No prior experience with Azure DevOps or CI/CD is required.
Set Up Your Accounts and Tools
A CI/CD pipeline needs two things to work: a platform to orchestrate it and tools to build your code.
In this step, you'll create a free Azure DevOps account, set up a project to hold your pipeline, and install Node.js and Git on your machine.
In this step, get ready to:
- Create an Azure DevOps organization and project.
- Install Node.js and Git on your machine.
- Verify everything is connected and ready.
Create your Azure DevOps organization and project
Azure DevOps is Microsoft's cloud platform for managing code, builds, and releases. You'll use it to host your repository and run your pipeline.
- Open your browser and navigate to https://dev.azure.com.
- Sign in with your Microsoft account.
- If you don't have a Microsoft account, click Create one and follow the prompts to set up a free account.
Why is Azure DevOps free?
Azure DevOps offers a free tier that includes the first 5 users free and 1 self-hosted CI/CD parallel job with unlimited minutes. No credit card is required to start.
- Once signed in, Azure DevOps prompts you to create an organization. Enter a name for your organization and select your closest region.
- After your organization is created, click New project in the top-right corner.
- Enter nodejs-pipeline-demo as the project name.
- Under Version control, make sure Git is selected.
- Leave the Work item process set to the default (Basic).
- Click Create.
Your project dashboard loads with a welcome page. You'll come back to this shortly.
Install Node.js and Git
Your pipeline will build and test a Node.js application, and Git tracks the code changes that trigger the pipeline. Both need to be installed on your local machine.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
What is PowerShell?
PowerShell is the command-line tool built into Windows. You'll use it throughout this project to run commands, configure your build agent, and interact with Git.
- Check if Node.js is already installed by running this command:
node --version
✔️ I see version 18 or higher
Your Node.js installation is ready. Move on to the npm check below.
ⓧ I see an older version
The project requires Node.js 18 or higher for the built-in test runner. You need to upgrade.
- Open your browser and go to https://nodejs.org.
- Download the LTS version (the large green button on the left).
- Run the installer and accept all default options.
- Close and reopen PowerShell after installation completes.
- Run node --version again to confirm the new version is 18 or higher.
ⓧ Command not found
Node.js is not installed on your machine yet.
- Open your browser and go to https://nodejs.org.
- Download the LTS version (the large green button on the left).
- Run the installer and accept all default options.
- Close and reopen PowerShell after installation completes.
- Run node --version again to confirm you see a version number.
- Verify npm is available by running this command:
npm --version
You should see a version number printed. npm is installed automatically alongside Node.js.
- Check if Git is already installed by running this command:
git --version
✔️ I see a version number
Git is ready to go. Move on to the next substep.
ⓧ Command not found
Git is not installed on your machine yet.
- Open your browser and go to https://git-scm.com.
- Click Download for Windows.
- Run the installer and accept all default options.
- Close and reopen PowerShell after installation completes.
- Run git --version again to confirm you see a version number.
Still seeing 'command not found' after installing?
Make sure you fully closed and reopened PowerShell after installation. The new commands are only recognized in a fresh terminal session.
If that doesn't help, try restarting your computer and opening PowerShell again.
help me troubleshoot Node.js or Git not being recognized in PowerShell after installation
Verify your Azure DevOps project is ready
Now confirm that your Azure DevOps project is set up correctly and you can access the sections you'll need.
- Return to your browser and navigate to your project. The URL follows the format https://dev.azure.com/your-organization-name/nodejs-pipeline-demo.
- In the left navigation menu, confirm you can see Repos.
- Confirm you can also see Pipelines in the same left menu.
What are Repos and Pipelines?
Repos is where Azure DevOps stores your Git repository. This is where your application code and pipeline configuration file will live.
Pipelines is where you create and monitor your automated builds. You'll use both of these throughout the project.
Your accounts and tools are ready. Next up, you'll configure a self-hosted build agent so your pipeline has a machine to run on.
Configure a Self-Hosted Build Agent
Your tools are installed and your Azure DevOps project is ready. Now you need something to actually run your pipeline.
In Azure DevOps, a build agent is the worker that executes your pipeline steps. Microsoft offers hosted agents, but new organizations are temporarily blocked from using them until a parallelism request is approved (which can take days or weeks). A self-hosted agent runs on your own machine, gives you unlimited free pipeline minutes, and starts immediately with no approval required.
In this step, get ready to:
- Download and install the Azure DevOps agent software on your machine.
- Create a Personal Access Token for agent authentication.
- Configure and start the agent so it appears online in your Default pool.
Download and extract the agent software
The agent software is a package you download directly from your Azure DevOps organization settings. You will extract it into a dedicated folder on your machine.
- In Azure DevOps, click the gear icon in the bottom-left corner to open Organization settings.
- In the left sidebar under Pipelines, click Agent pools.
- Select the Default pool from the list.
- Click the Agents tab, then click New agent.
- In the dialog that appears, select Windows and click Download to download the agent ZIP file.
Now create the folder and extract the agent files. Open PowerShell and create the agent directory by running this command:
mkdir C:\agents
- Open File Explorer and navigate to your Downloads folder.
- Right-click the downloaded agent ZIP file and select Extract All.
- Set the destination to C:\agents and click Extract.
Why C:\agents?
The folder path must not contain spaces because the agent's tools and scripts do not always handle spaces correctly. C:\agents is the recommended location from Microsoft's official documentation.
- Confirm the extraction worked by opening C:\agents in File Explorer. You should see files including config.cmd and run.cmd.
Don't see config.cmd in C:\agents?
If C:\agents contains a single subfolder instead of the agent files, the ZIP was extracted with an extra nesting layer. Move all contents from the subfolder up into C:\agents directly.
Help me fix my agent extraction so config.cmd is in C:\agents
Create a Personal Access Token for authentication
The agent needs a credential to register itself with your Azure DevOps organization. You will create a Personal Access Token (PAT) scoped specifically to agent pool management.
- In Azure DevOps, click your profile icon in the top-right corner.
- Select Personal access tokens from the dropdown menu.
- Click + New Token.
- In the Name field, enter pipeline-agent.
- Leave the Organization set to your current organization.
- Set an expiration date (30 days is fine for this project).
- Click Show all scopes at the bottom of the dialog to see the full list.
- Scroll down to Agent Pools and check Read & manage. Make sure all other scopes are unchecked.
- Click Create.
- Copy the generated token immediately and save it somewhere safe (like a temporary text file). You will not be able to see it again after closing this dialog.
What is a Personal Access Token?
A PAT is a security credential that acts like a password for automated tools. It grants only the specific permissions you select (in this case, managing agent pools). The token is used only during agent registration and is not stored on the agent afterwards.
Can't find Agent Pools in the scopes list?
Make sure you clicked Show all scopes at the bottom of the token creation dialog. Agent Pools is not visible in the default simplified view.
Help me find the Agent Pools scope when creating a PAT
Configure and start the agent
With the agent software extracted and your PAT ready, you can now configure the agent to connect to your organization. This requires an elevated PowerShell window.
- Press the Windows key to open your search bar.
- Type PowerShell, right-click Windows PowerShell, and select Run as administrator.
Why elevated PowerShell?
The agent configuration requires administrator privileges to register system services. Do not use Windows PowerShell ISE or Git Bash for this step. Standard PowerShell run as administrator is what you need.
- Navigate to the agent folder by running this command:
cd C:\agents
- Start the agent configuration by running:
.\config.cmd
The configuration script asks a series of questions. Answer each prompt as follows:
- Enter server URL: type your organization URL in the format https://dev.azure.com/{your-org} (replace {your-org} with your actual organization name).
- Enter authentication type: press Enter to accept the default (PAT).
- Enter personal access token: paste the PAT you copied earlier.
- Enter agent pool: press Enter to accept the default (Default).
- Enter agent name: type my-windows-agent and press Enter.
- Enter work folder: press Enter to accept the default.
- If asked to run the agent as a service: enter N (you will run it interactively for this project).
After configuration completes, you should see a message confirming the agent was successfully configured.
- Start the agent by running:
.\run.cmd
You should see output indicating the agent is listening for jobs. The last line will say something similar to "Listening for Jobs".
What is happening when the agent runs?
The agent connects to Azure DevOps using HTTP long polling, continuously checking for new pipeline jobs. When a pipeline triggers, Azure DevOps sends the job to your agent, which then executes each step locally on your machine.
Keep this PowerShell window open for the rest of the project. Closing it stops the agent.
- Verify the agent is online by going back to Azure DevOps in your browser.
- Navigate to Organization settings > Agent pools > Default > Agents tab.
- You should see my-windows-agent listed with a green "Online" status.
Agent showing as Offline?
Make sure the PowerShell window running .\run.cmd is still open and has not been closed or interrupted.
If you see an authentication error during configuration, double-check that your PAT has the Agent Pools (Read & manage) scope and that you entered the correct organization URL.
Help me troubleshoot my self-hosted agent not showing as online
Why a self-hosted agent instead of Microsoft-hosted?
Microsoft has temporarily disabled the automatic free grant of parallel jobs for new Azure DevOps organizations. Requesting access can take several business days with no guaranteed timeline. A self-hosted agent bypasses this entirely.
Self-hosted agents also give you 1 free parallel job with unlimited minutes per organization. No credit card or approval needed.
Your self-hosted agent is now online and waiting for work. Next up, you will create the Node.js application that your pipeline will build and test.
Create a Node.js Application with Tests
Your self-hosted agent is running and connected to Azure DevOps. Now you need an application for it to actually build and test.
In this step, you'll create a simple Node.js math utility module with automated tests. This gives your pipeline something meaningful to work with when you wire it up in the next step.
In this step, get ready to:
- Clone the Azure Repos repository to your local machine.
- Create a Node.js application with math utility functions.
- Write automated tests and push everything to Azure Repos.
Initialize and clone the repository
Before you can add code, you need to initialize the repository in Azure Repos and clone it to your local machine.
- In your Azure DevOps project, click Repos in the left navigation menu.
- Click Files.
- Make sure the Add a README checkbox is checked.
- Click Initialize.
What does Initialize do?
Initializing creates the first commit in your repository with a README file. This sets up the main branch so you have something to clone.
- After the repository initializes, click Clone in the upper-right area of the page.
- Copy the HTTPS clone URL. It follows this format:
https://dev.azure.com/[[AZUREORG="your-organization-name"]]/nodejs-pipeline-demo/_git/nodejs-pipeline-demo
- Open a new PowerShell window. Your agent is already running in another window, so use a fresh one for development work.
- Navigate to your Desktop by running this command:
cd ~/Desktop
- Clone the repository by running this command:
git clone [[CLONE_URL="your clone URL from Azure DevOps"]]
Paste your actual clone URL
Replace the placeholder with the URL you copied from Azure DevOps. It will look like https://dev.azure.com/your-org/nodejs-pipeline-demo/_git/nodejs-pipeline-demo.
- Git may prompt you to sign in. Use the same Microsoft account you used for Azure DevOps.
- Navigate into the cloned folder by running:
cd nodejs-pipeline-demo
- Confirm you are inside the repository by running:
ls
You should see a README.md file listed in the output.
Clone failed or no README shown?
Make sure you copied the full HTTPS URL from the Clone button in Azure DevOps. The URL must end with /_git/nodejs-pipeline-demo.
If you see a credential error, check that you signed in with the same Microsoft account you used to create your Azure DevOps organization.
Help me troubleshoot my clone issue
Create the application and test files
Now you'll create a simple math utility module and matching tests. Node.js has a built-in test runner (node:test) so you don't need any external testing frameworks.
- Initialize a Node.js project by running this command:
npm init -y
You should see a confirmation message showing a new package.json file was created.
What does npm init -y do?
This command generates a package.json file with default values. The -y flag skips the interactive prompts and accepts all defaults automatically.
- Create a new file called index.js in the nodejs-pipeline-demo folder. Open it in your preferred text editor and add the following code:
// Math utility functions
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Export functions so tests can use them
module.exports = { add, multiply };
What does this code do?
- add(a, b) returns the sum of two numbers.
- multiply(a, b) returns the product of two numbers.
- module.exports makes these functions available to other files (like your test file).
- Save index.js.
- Create a test folder by running:
mkdir test
- Create a new file called index.test.js inside the test folder. Open it in your text editor and add the following code:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, multiply } = require('../index');
describe('Math utilities', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(add(-1, 1), 0);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
assert.strictEqual(multiply(0, 5), 0);
});
});
What does this test file do?
- node:test is Node.js's built-in test runner. It provides describe and it functions to organize tests into groups.
- node:assert provides assertion methods like strictEqual that compare expected vs actual values.
- require('../index') imports the functions from your index.js file so the tests can call them.
- Save test/index.test.js.
Not sure where to create the files?
Both index.js and the test folder should be at the top level of your nodejs-pipeline-demo folder, next to package.json and README.md. You can use any text editor (Notepad, VS Code, or others) to create and edit files.
Help me create files in the correct location
Configure, test, and push to Azure Repos
The package.json generated by npm init -y has a placeholder test script. You need to update it to use Node.js's built-in test runner before running your tests.
- Open package.json in your text editor.
- Find this line inside the "scripts" section:
"test": "echo \"Error: no test specified\" && exit 1"
- Replace the entire contents of package.json with the following:
{
"name": "nodejs-pipeline-demo",
"version": "1.0.0",
"description": "A Node.js app with CI/CD pipeline in Azure DevOps",
"main": "index.js",
"scripts": {
"test": "node --test"
},
"keywords": [],
"author": "",
"license": "ISC"
}
What changed?
- The "test" script now runs node --test instead of the placeholder error message.
- node --test tells Node.js to find and run all test files in the test folder automatically.
- The description field now accurately describes your project.
- Save package.json.
- Run your tests locally to verify everything works:
npm test
You should see output indicating that both tests passed. Look for a summary showing 2 tests passed and 0 failed.
Tests failing?
If you see an error about node:test not being found, your Node.js version may be too old. The built-in test runner requires Node.js 18 or higher. Run node --version to check.
If you see a module-not-found error, make sure index.js is in the same folder as package.json, and that index.test.js is inside the test subfolder.
Help me fix my failing tests
- Now commit and push all your files to Azure Repos by running these commands:
git add .
git commit -m "Add Node.js app with tests"
git push
What do these commands do?
- git add . stages all new and modified files for commit.
- git commit -m "Add Node.js app with tests" saves a snapshot of your staged changes with a descriptive message.
- git push uploads your local commits to the remote repository on Azure DevOps.
- Verify your push succeeded by switching back to Azure DevOps in your browser.
- Click Repos in the left navigation, then click Files.
You should see index.js, package.json, and the test folder listed alongside README.md.
Push was rejected?
If Git rejects your push, you may need to set your Git identity first. Run these commands with your own name and email, then try the commit and push again:
git config user.name "Your Name"
git config user.email "your.email@example.com"
Help me fix my git push issue
✔️ Awesome, I've got everything!
Great. Double check that you've saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is the full content of each file you should have at this point.
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, multiply } = require('../index');
describe('Math utilities', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(add(-1, 1), 0);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
assert.strictEqual(multiply(0, 5), 0);
});
});
{
"name": "nodejs-pipeline-demo",
"version": "1.0.0",
"description": "A Node.js app with CI/CD pipeline in Azure DevOps",
"main": "index.js",
"scripts": {
"test": "node --test"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Your application is ready and stored in Azure Repos. Next up, you'll create a YAML pipeline that automatically builds and tests this code every time you push a change.
Create Your First YAML Pipeline
Your Node.js application is committed to Azure Repos with passing tests. Now it's time to tell Azure DevOps what to do with that code every time you push a change.
You'll define your pipeline as code using a YAML file. This file lives in your repository alongside your application code. Every time you push to the main branch, Azure Pipelines reads this file and executes the steps you defined.
In this step, get ready to:
- Write a YAML pipeline configuration file.
- Connect the pipeline to Azure DevOps and run it.
- Verify both pipeline steps pass with green checkmarks.
Create the pipeline configuration file
A YAML pipeline file tells Azure Pipelines exactly what to run, on which agent pool, and when to trigger.
- Create a new file called azure-pipelines.yml in the root of your nodejs-pipeline-demo folder by running this command:
New-Item azure-pipelines.yml
- Confirm the file exists by running:
ls
You should see azure-pipelines.yml listed alongside index.js, package.json, and the test folder.
- Open azure-pipelines.yml in your text editor and paste the following configuration:
# Trigger the pipeline on pushes to main
trigger:
- main
# Route jobs to the self-hosted agent pool
pool:
name: Default
# Define the build and test steps
steps:
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
What does this configuration do?
This YAML file defines three things for your pipeline:
- trigger: - main tells Azure Pipelines to run this pipeline every time code is pushed to the main branch.
- pool: name: Default routes the work to your self-hosted agent instead of requesting a Microsoft-hosted agent.
- Each script step runs a shell command on your agent machine. The first installs your Node.js dependencies, and the second runs your tests.
- Save azure-pipelines.yml.
Don't see the file?
Make sure you created the file inside the nodejs-pipeline-demo folder (the root of your cloned repository), not a subfolder. Run pwd to confirm your current location.
Help me find my pipeline file.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
trigger:
- main
pool:
name: Default
steps:
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
Commit and push your pipeline to Azure Repos
The pipeline file needs to be in your remote repository for Azure DevOps to find it. You'll push it up and then connect it through the Pipelines UI.
- Stage, commit, and push the pipeline file by running these commands:
git add azure-pipelines.yml
git commit -m "Add pipeline configuration"
git push
What do these commands do?
git add stages the new file for commit. git commit saves a snapshot locally. git push uploads the commit to Azure Repos so the pipeline can find the YAML file.
- Verify the push succeeded. You should see output ending with a line like main -> main confirming the branch was updated.
Push rejected or authentication error?
If Git asks for credentials, use your Microsoft account email and a Personal Access Token (not your password). The same PAT you created earlier may not have Code permissions. Create a new PAT with Code (Read & Write) scope if needed.
Help me troubleshoot my git push credentials.
Now connect this YAML file to a pipeline in Azure DevOps.
- Switch back to your Azure DevOps project in the browser.
- Click Pipelines in the left navigation menu.
- Click New pipeline.
- Select Azure Repos Git as your code source.
- Select your nodejs-pipeline-demo repository.
- On the Configure page, select Existing Azure Pipelines YAML file.
- Leave the branch as main and select /azure-pipelines.yml from the path dropdown.
- Click Continue.
- Review the YAML preview. Click Save and run.
- In the commit dialog, click Save and run again to confirm.
Why Existing Azure Pipelines YAML file?
This option tells Azure DevOps to look for a YAML file already committed to your repository. You wrote and pushed the file yourself, so Azure DevOps reads your exact configuration instead of generating a starter template.
Watch the pipeline execute
Azure DevOps starts running your pipeline immediately after you click Save and run. You can watch each step execute in real time.
- Click on the running job (it shows as Job or the agent name) to open the live log view.
- Watch the Install dependencies step complete. You should see npm install output scrolling through.
- Watch the Run tests step complete. You should see your test results and green checkmarks next to both steps.
What should I see?
Both steps display green checkmarks when they pass. The Run tests step should show output confirming your math utility tests passed (2 tests, 0 failures).
Pipeline stuck on queued or agent offline?
Switch back to the PowerShell window where you ran .\run.cmd. Make sure the agent is still running and shows 'Listening for Jobs'. If it stopped, run .\run.cmd again from C:\agents.
Help me troubleshoot my queued pipeline.
Your first CI pipeline is live. Every push to main now triggers an automatic build and test run. Next up, you'll expand this into a multi-stage pipeline with separate Build and Test stages.
Expand to a Multi-Stage Pipeline
Your pipeline is working. Every push to main automatically installs dependencies and runs tests in a single block of steps.
Real-world pipelines split work into separate stages. Each stage handles one responsibility (building, testing, deploying) and runs independently. In this step, you'll restructure your pipeline into a multi-stage YAML pipeline with separate Build and Test stages, then prove that CI works automatically by pushing a code change.
In this step, get ready to:
- Rewrite your pipeline YAML to use a multi-stage structure with Build and Test stages.
- Watch both stages execute in sequence in Azure DevOps.
- Trigger CI automatically by adding a new feature to your code.
Edit your pipeline to use stages
A stage is a logical boundary in your pipeline. Stages group jobs together, and you can define dependencies between them so one stage must pass before the next begins.
- In your code editor, open azure-pipelines.yml.
- Replace the entire contents of the file with the following multi-stage pipeline:
trigger:
- main
stages:
# Stage 1: Install project dependencies
- stage: Build
displayName: 'Build Stage'
jobs:
- job: BuildJob
displayName: 'Install Dependencies'
pool:
name: Default
steps:
- script: |
npm install
displayName: 'Install dependencies'
# Stage 2: Run tests (only if Build succeeds)
- stage: Test
displayName: 'Test Stage'
dependsOn: Build
jobs:
- job: TestJob
displayName: 'Run Tests'
pool:
name: Default
steps:
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
What does this pipeline do?
- stages: replaces the flat steps: structure. Each stage is an independent unit of work.
- The Build stage installs dependencies with npm install. Its only job is to confirm the project can be set up successfully.
- The Test stage uses dependsOn: Build to declare that it will only run if the Build stage passes.
- Each stage re-runs npm install because each job starts in a fresh working directory context, even on a self-hosted agent.
Why separate Build and Test stages?
In a flat pipeline, a test failure is buried in the same list as install steps. With stages, you can see at a glance whether the failure was in building or testing. This separation becomes critical as pipelines grow to include deployment, security scanning, or artifact publishing.
- Save azure-pipelines.yml.
You won't see a change yet. The next substep pushes this file and triggers the pipeline.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
trigger:
- main
stages:
- stage: Build
displayName: 'Build Stage'
jobs:
- job: BuildJob
displayName: 'Install Dependencies'
pool:
name: Default
steps:
- script: |
npm install
displayName: 'Install dependencies'
- stage: Test
displayName: 'Test Stage'
dependsOn: Build
jobs:
- job: TestJob
displayName: 'Run Tests'
pool:
name: Default
steps:
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
Commit, push, and watch both stages execute
- Commit and push the updated pipeline by running these commands in PowerShell:
git add azure-pipelines.yml
git commit -m "Convert to multi-stage pipeline"
git push
What do these commands do?
- git add azure-pipelines.yml stages the updated file for commit.
- git commit saves the change to your local repository history.
- git push sends the commit to Azure Repos, which triggers the pipeline because of the trigger: - main setting.
- Switch back to your browser and navigate to Pipelines in the left navigation menu of your Azure DevOps project.
- Click on the running pipeline to open it.
- You should see two stages displayed: Build Stage and Test Stage, connected with an arrow showing their dependency.
- Wait for both stages to complete with green checkmarks.
Pipeline not showing two stages?
- Make sure you saved the file before committing. Run git status to check whether the file shows as modified.
- Verify that your YAML indentation is correct. YAML is whitespace-sensitive, so misaligned lines cause parsing errors.
- Check that your self-hosted agent is still running. Look at the PowerShell window where you ran .\\run.cmd and confirm it shows a listening state.
Why is my multi-stage pipeline failing or not showing stages?
Trigger CI automatically with a code change
The real power of CI is that it runs automatically on every push. To prove this, you'll add a new feature to your application and watch the pipeline trigger on its own.
- In your code editor, open index.js.
- Find the multiply function and the module.exports line at the bottom of the file.
- Replace the entire file contents with the following code, which adds a new subtract function:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// New feature: subtract two numbers
function subtract(a, b) {
return a - b;
}
module.exports = { add, multiply, subtract };
What's new here?
You added a subtract function that takes two numbers and returns their difference. The module.exports line now includes subtract so other files can import and use it.
- Save index.js.
You won't see a change yet. The next chunk adds a test for this function.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.js before moving on.
ⓧ I'd like to double check the full code
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, multiply, subtract };
- Open test/index.test.js.
- Replace the entire file contents with the following code, which adds a test for subtract and updates the import:
const { describe, it } = require('node:test');
const assert = require('node:assert');
// Import all three functions including the new subtract
const { add, multiply, subtract } = require('../index');
describe('Math utilities', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(add(-1, 1), 0);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
assert.strictEqual(multiply(0, 5), 0);
});
// New test for the subtract function
it('should subtract two numbers', () => {
assert.strictEqual(subtract(10, 4), 6);
assert.strictEqual(subtract(0, 3), -3);
});
});
What changed in this file?
- The require('../index') line now destructures subtract alongside add and multiply.
- A new it('should subtract two numbers') block verifies that subtract(10, 4) returns 6 and subtract(0, 3) returns -3.
- Save test/index.test.js.
- Verify your tests pass locally by running:
npm test
- You should see output showing 3 passing tests (add, multiply, and subtract).
Tests failing locally?
- Check that subtract is spelled the same in both index.js and test/index.test.js. A mismatch causes an import error.
- Confirm the module.exports line in index.js includes all three functions: { add, multiply, subtract }.
- Make sure you saved both files before running npm test.
Why are my Node.js tests failing after adding the subtract function?
✔️ Awesome, I've got everything!
Great. Make sure both files are saved and tests pass before pushing.
ⓧ I'd like to double check the full code
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, multiply, subtract } = require('../index');
describe('Math utilities', () => {
it('should add two numbers', () => {
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(add(-1, 1), 0);
});
it('should multiply two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
assert.strictEqual(multiply(0, 5), 0);
});
it('should subtract two numbers', () => {
assert.strictEqual(subtract(10, 4), 6);
assert.strictEqual(subtract(0, 3), -3);
});
});
- Commit and push all your changes by running:
git add .
git commit -m "Add subtract function"
git push
- Switch back to Pipelines in Azure DevOps.
- Watch the pipeline trigger automatically. You did not click anything to start it. The push to main triggered it.
- Click on the new pipeline run and confirm both the Build and Test stages pass with green checkmarks.
Pipeline didn't trigger automatically?
- Confirm your push succeeded by checking the output of git push for errors.
- Verify the trigger: section in azure-pipelines.yml includes - main.
- Check that your self-hosted agent is still online. Return to the PowerShell window running .\run.cmd and look for connection messages.
Why didn't my Azure DevOps pipeline trigger automatically after I pushed?
Your multi-stage pipeline is live and proving that Continuous Integration works. Every commit now flows through Build and Test stages automatically, catching issues before they reach production.
Secret mission
Add a Deploy Stage with Artifact Publishing
Your pipeline builds and tests, but in a real project you also need to package your application for deployment. Add a third stage that packages your app as a downloadable artifact.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely on Azure DevOps's free tier and a self-hosted agent on your local machine. There are no ongoing cloud costs.
Resources you used:
- Azure DevOps organization and project nodejs-pipeline-demo (free tier, no charges).
- Self-hosted agent my-windows-agent installed at C:\agents.
- Personal Access Token pipeline-agent used for agent authentication.
- Local clone of the nodejs-pipeline-demo repository on your machine.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with more pipelines.
- Your self-hosted agent continues running in the PowerShell window. It costs nothing and is ready for new pipeline runs.
- Your Azure DevOps project stays on the free tier with no charges.
- Your Personal Access Token remains active until its expiry date.
Pause - I'll come back to this later
Stop the agent to free up your PowerShell window. You can restart anytime without reconfiguring.
- In the PowerShell window where your agent is running, press Ctrl+C to stop the agent.
- When you're ready to resume, open PowerShell, navigate to C:\agents, and run .\run.cmd to start the agent again.
- Your Azure DevOps project and pipeline configuration remain intact. No data is lost.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Stop the agent by pressing Ctrl+C in the PowerShell window where it is running.
- Remove the agent configuration by running this command from C:\agents:
cd C:\agents
.\config remove
When prompted, enter your Personal Access Token to authorize the removal.
- Delete the C:\agents folder by running:
rmdir /s /q C:\agents
- Revoke your Personal Access Token. In Azure DevOps, click your profile icon in the top right, select Personal access tokens, find pipeline-agent, and click Revoke.
- Delete the Azure DevOps project. Go to Organization settings (bottom left), select Projects, check the box next to nodejs-pipeline-demo, and click Delete. Confirm by entering the project name when prompted.
- Delete your local repository clone by running:
rmdir /s /q nodejs-pipeline-demo
Run this command from the folder where you originally cloned the repository.
Nice Work!
Nice Work!
You just built a real CI/CD pipeline from scratch. Every time you push code to your repository, your pipeline automatically builds and tests your application without any manual intervention.
Here's what you accomplished:
- Configured a self-hosted Windows build agent for unlimited free pipeline runs in Azure DevOps.
- Wrote a YAML pipeline that automatically builds and tests your Node.js application on every commit to the main branch.
- Expanded to a multi-stage pipeline with separate Build and Test stages that demonstrates real-world CI/CD structure.
- Packaged your application as a downloadable pipeline artifact using a Deploy stage with CopyFiles@2 and PublishPipelineArtifact@1.
Ready to quiz yourself?