Build a CI/CD Pipeline with Azure DevOps
Build a multi-stage CI/CD pipeline that auto-builds, tests, and deploys code.
Introduction
30 Second Summary
Every time you make a change to your code, someone has to run the tests, package everything up, and copy files to a server. Most teams do this dozens of times a day. Doing it by hand is slow, error-prone, and exhausting.
In this project, you will build a CI/CD pipeline in Azure DevOps that automatically builds, tests, and deploys a Node.js application every time you push code.
What You'll Build
A fully automated multi-stage CI/CD pipeline that triggers on every code push, runs your tests, and deploys your application using a YAML configuration file stored in your repository.
By the end of this project, you'll have:
- An Azure DevOps organization and project with a Git repository storing your Node.js application.
- A self-hosted agent running on your machine that executes pipeline jobs.
- A multi-stage YAML pipeline that builds, tests, and deploys your code automatically on every push.
- Secret Mission: Add a manual approval gate to control production deployments.
Are there any prerequisites?
No prior DevOps experience is needed. This guide walks you through setting up every tool from scratch, including Node.js, Git, and Azure DevOps. No credit card is required to create your Azure DevOps account.
Set Up Azure DevOps and Tools
Before you can build a pipeline, you need the tools that power it. Your pipeline will run a Node.js application, so you need Node.js installed locally to create and test the app.
You also need Git to push your code to a repository, and an Azure DevOps account to host your pipeline. In this step, you will install both tools and create your Azure DevOps organization.
In this step, get ready to:
- Install Node.js and verify it works.
- Install Git and configure your identity.
- Create an Azure DevOps account and organization.
Install Node.js
Node.js is the JavaScript runtime your application will use. It also includes npm, the package manager you will use to install dependencies and run tests.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Node.js is already installed by running this command:
node --version
✔️ I see a version number
Node.js is already installed. Now confirm that npm is available by running:
npm --version
You should see a version number printed. You are all set to move on to Git.
ⓧ Command not found
Node.js is not installed yet. Follow these steps to install it.
- Open your browser and go to nodejs.org.
- Click the LTS download button (this is the stable, recommended version).
- Run the downloaded installer and accept all defaults.
- Close and reopen your terminal so it picks up the new installation.
- Verify the install by running:
node --version
- Then confirm npm is available by running:
npm --version
Both commands should print a version number.
Still seeing command not found after installing?
Make sure you closed and reopened your terminal after the installation. New programs are only recognized in a fresh terminal session.
On Windows, you may need to restart your entire computer for the PATH to update. Help me troubleshoot my Node.js installation.
Install Git
Git is the version control system that Azure DevOps uses to store your code. Every time you push a change, Git triggers your pipeline to run.
- Check if Git is already installed by running this command:
git --version
✔️ I see a version number
Git is already installed. Skip ahead to the identity configuration below.
ⓧ Command not found
Git is not installed yet. Follow these steps to install it.
- Open your browser and go to git-scm.com.
- Click Download for Windows or Download for Mac (whichever matches your system).
- Run the installer and accept all defaults.
- Close and reopen your terminal.
- Verify the install by running:
git --version
You should now see a version number printed.
Git needs to know who you are so it can label each commit with your name and email. This identity is attached to every change you push.
- Configure your Git identity by running these commands:
git config --global user.name "[[YOUR_NAME=\"Your Name\"]]"
git config --global user.email "[[YOUR_EMAIL=\"your-email@example.com\"]]"
What do these commands do?
The --global flag sets your identity for all Git repositories on your machine.
Replace the placeholder name and email with your own. Use the same email you plan to sign into Azure DevOps with.
- Verify your identity was saved by running:
git config --global --list
You should see your user.name and user.email listed in the output.
Don't see your name and email?
Make sure you included the quotes around your name and email in the commands above. Without quotes, Git may only capture the first word.
Help me fix my Git configuration.
Create an Azure DevOps account and organization
Azure DevOps is the platform where your pipeline will live. An organization is the top-level container that holds all your projects, repositories, and pipelines.
Is this free?
Yes. Azure DevOps is free to sign up with no credit card required. You get 1 free self-hosted parallel job with unlimited minutes, which is all you need for this project.
- Open your browser and go to https://dev.azure.com.
- Click Start free.
- Sign in with a Microsoft account or a GitHub account. If you do not have either, create a free Microsoft account when prompted.
- After signing in, Azure DevOps asks you to create a new organization. Enter a name for your organization (for example, yourname-devops).
- Choose a region closest to you from the dropdown.
- Click Continue.
You should land on your new organization's home page. The URL in your browser should look like https://dev.azure.com/your-org-name.
What is an organization in Azure DevOps?
An organization is your top-level workspace. It holds one or more projects, and each project can contain repositories, pipelines, and boards.
Think of it like a company account that groups all your DevOps work in one place.
Stuck on sign-in or organization creation?
If you are asked to verify your identity, follow the email or phone verification steps. This is a one-time security check for new accounts.
Help me get past the Azure DevOps sign-up process.
Your development tools are installed and your Azure DevOps organization is ready. Next up, you will create a project inside this organization and set up your code repository.
Create Your Project and App
Your Azure DevOps organization is ready. Now you need a project inside it to hold your code and your pipeline.
In this step, you will create a project in Azure DevOps, clone its Git repository to your machine, and build a simple Node.js application with tests. This app is what your pipeline will build and deploy automatically in later steps.
In this step, get ready to:
- Create an Azure DevOps project with a Git repository.
- Clone the repository to your local machine.
- Build a Node.js app with automated tests.
Create a new project in Azure DevOps
A project in Azure DevOps is the container that holds your source code repository, pipelines, and work items. You need one before you can store or build any code.
- In your browser, go to your Azure DevOps organization at https://dev.azure.com.
- Click New project on the Projects page.
- Enter node-cicd-demo as the project name.
- Set Visibility to Private.
- Under Advanced, select Git for version control.
- Select Agile for the work item process.
- Click Create.
What is a project in Azure DevOps?
A project isolates your code, pipelines, boards, and artifacts under one roof. Think of it as a workspace for a single application or team.
The Git version control option gives you a distributed repository. The Agile process template provides a lightweight board for tracking work items.
Azure DevOps displays the project welcome page after creation.
Clone the repository to your machine
Your new project includes an empty Git repository. You need a local copy of it so you can add application code from your terminal.
- In Azure DevOps, click Repos in the left sidebar.
- Click Clone in the upper-right corner of the Files view.
- Copy the HTTPS clone URL from the popup.
What does a clone URL look like?
Your clone URL follows the format https://dev.azure.com/{your-org}/node-cicd-demo/_git/node-cicd-demo. This uniquely identifies your remote repository.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the repository and move into it by running these commands:
git clone [[CLONE_URL="your HTTPS clone URL from Azure Repos"]]
cd node-cicd-demo
What does git clone do?
It downloads the full repository (including all branches and history) into a new folder on your machine. The folder is named node-cicd-demo to match the repository.
- Confirm you are inside the node-cicd-demo folder by running:
ls
The folder may be empty or contain only a README.md file. That is expected for a freshly created repository.
Clone failing with an authentication error?
Git may prompt you to sign in via a browser window. Accept the prompt and authenticate with the same account you used for Azure DevOps.
If you see a 403 error, confirm you are using the HTTPS URL (not SSH) and that your Git Credential Manager is installed.
help me fix a git clone authentication error with Azure DevOps
Build the Node.js application
Your pipeline needs something to build and test. You will create a small Express web server with two routes, plus a test file that verifies the app exports correctly.
- Initialize a new Node.js package by running this command:
npm init -y
What does npm init -y do?
It creates a package.json file with default values. The -y flag skips all the interactive prompts and accepts the defaults.
- Install Express (your web framework) by running:
npm install express
- Install Mocha and Chai (your testing tools) as dev dependencies by running:
npm install --save-dev mocha chai
Why mocha and chai?
Mocha is a test runner that executes your test files and reports pass/fail. Chai is an assertion library that provides readable check syntax like expect(value).to.equal(something).
Together they give your pipeline a way to automatically verify your app works on every push.
- Confirm the packages installed by running:
ls node_modules
You should see folders for express, mocha, chai, and their sub-dependencies listed.
npm install hanging or failing?
Check your internet connection. If you are behind a corporate proxy, you may need to configure npm with your proxy settings.
If you see permission errors on macOS, do not use sudo. Instead, fix npm permissions by following the official npm guide.
help me fix npm install errors
Now you need to update package.json with the correct scripts so your pipeline knows how to start and test the app.
- Open package.json in your text editor.
- Find the "scripts" section and the "description" field.
- Replace the "scripts" block and "description" value to match the following:
{
"name": "node-cicd-demo",
"version": "1.0.0",
"description": "A simple Node.js app for learning Azure DevOps CI/CD",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "mocha test.js"
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"mocha": "^10.7.0",
"chai": "^4.4.1"
}
}
What do these scripts do?
The start script tells Node.js to launch your server. The test script tells Mocha to run your test file.
Your pipeline will call npm test later to verify the app works before deploying it.
Version numbers may differ slightly
Your dependency versions depend on what was latest when you ran npm install. If they are slightly different from the example, that is fine. The important parts are the scripts and description fields.
- Save package.json.
Next, create the application server.
- Create a file called index.js in the node-cicd-demo folder and paste the following code:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// Root route returns a greeting
app.get('/', (req, res) => {
res.json({ message: 'Hello from the CI/CD pipeline!' });
});
// Health check route returns the server status
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Only start the server if this file is run directly
if (require.main === module) {
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
// Export the app for testing
module.exports = app;
What does this code do?
It creates a minimal Express server with two endpoints. The / route returns a JSON greeting. The /health route returns a status check with the current time.
The if (require.main === module) block makes the server start only when you run node index.js directly. When tests import the file, the server does not start, which prevents port conflicts.
The module.exports = app line lets your test file import and inspect the app object.
- Save index.js.
Finally, create the test file that will validate your app during the pipeline run.
- Create a file called test.js in the node-cicd-demo folder and paste the following code:
const chai = require('chai');
const expect = chai.expect;
const app = require('./index');
// Test suite for the Express app
describe('App', function() {
// Verify the app exports correctly
it('should export the app', function() {
expect(app).to.be.a('function');
});
// Verify routes are registered
it('should have a root route', function() {
expect(app._router).to.not.be.undefined;
});
});
What do these tests check?
The first test confirms that index.js exports a function (the Express app). The second test confirms that routes are registered by checking that app._router exists.
These are simple smoke tests. In a real project, you would add more thorough tests for each endpoint. For this project, they are enough to prove your pipeline can run automated checks.
- Save test.js.
- Verify everything works by running your tests:
npm test
You should see output showing 2 passing tests with green checkmarks.
Tests failing or command not found?
If you see a "mocha: command not found" error, make sure your package.json has "test": "mocha test.js" in the scripts section. The npm test command uses that script to find mocha in your local node_modules folder.
If a test fails, double check that index.js includes module.exports = app at the bottom of the file.
help me debug failing npm test
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: package.json, index.js, and test.js.
ⓧ I'd like to double check the full code
Here is the complete code for each file. Compare yours against these.
{
"name": "node-cicd-demo",
"version": "1.0.0",
"description": "A simple Node.js app for learning Azure DevOps CI/CD",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "mocha test.js"
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"mocha": "^10.7.0",
"chai": "^4.4.1"
}
}
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ message: 'Hello from the CI/CD pipeline!' });
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
if (require.main === module) {
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
module.exports = app;
const chai = require('chai');
const expect = chai.expect;
const app = require('./index');
describe('App', function() {
it('should export the app', function() {
expect(app).to.be.a('function');
});
it('should have a root route', function() {
expect(app._router).to.not.be.undefined;
});
});
Your app is built and tested locally. Next up, you will push this code to Azure Repos and create the YAML pipeline that automates the entire build-and-test process.
Push Code and Create the CI Pipeline
Your Node.js application is built, tested locally, and ready to go. Now it is time to put it to work in a real CI/CD pipeline.
In this step, you will push your code to Azure Repos and create an azure-pipelines.yml file that tells Azure DevOps exactly how to build and test your app every time you push a change.
In this step, get ready to:
- Push your application code to Azure Repos.
- Create a YAML pipeline file that defines the CI process.
- Commit and push the pipeline configuration.
Push your code to Azure Repos
Your application files only exist on your local machine right now. Pushing them to Azure Repos makes them available to the pipeline you will create next.
- Stage, commit, and push all your files to the main branch by running these commands:
git add .
git commit -m "Initial commit with Node.js app"
git push origin main
What do these commands do?
- git add . stages every file in your project folder for the next commit.
- git commit -m "..." saves a snapshot of those staged files with a descriptive message.
- git push origin main uploads that snapshot to the main branch on Azure Repos.
Seeing a sign-in prompt?
When you push for the first time, your system may open a browser window asking you to sign in with your Microsoft account. This is normal. Sign in with the same account you used to create your Azure DevOps organization.
- Open your Azure DevOps project in the browser and click Repos in the left sidebar.
- You should see index.js, test.js, and package.json listed in the file tree.
Files not showing up in Repos?
- Make sure your terminal is inside the node-cicd-demo folder. Run pwd (macOS/Linux) or cd (Windows) to confirm.
- Check for typos in the remote URL. Run git remote -v and confirm it points to your Azure DevOps repository.
- If authentication failed, try running git push origin main again and sign in when prompted.
help me push my code to Azure Repos
Create the pipeline YAML file
A YAML pipeline file lives in your repository alongside your code. It defines every step Azure DevOps should run when changes are pushed.
- Create a file called azure-pipelines.yml in the root of your node-cicd-demo folder and paste in the following code:
# Define which branch triggers the pipeline
trigger:
branches:
include:
- main
# Use the self-hosted agent pool (you'll set this up in the next step)
pool:
name: 'my-local-agent'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
# Install Node.js on the agent
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
# Install project dependencies
- script: npm install
displayName: 'Install dependencies'
# Run the test suite
- script: npm test
displayName: 'Run tests'
# Copy app files (excluding node_modules) to a staging folder
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
**/*
!node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy files to staging'
# Publish the staged files as a downloadable artifact
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'node-app'
displayName: 'Publish artifact'
What does this pipeline do?
The trigger section tells Azure DevOps to run this pipeline whenever code is pushed to the main branch.
The pool section specifies that jobs run on your self-hosted agent pool called my-local-agent. You will set this up in the next step.
The Build stage does four things in order: installs Node.js 18.x using NodeTool@0, installs your npm dependencies, runs your Mocha tests, and then packages the app files into a build artifact called node-app.
Why exclude node_modules?
The node_modules folder contains thousands of dependency files that can be recreated by running npm install. Excluding it keeps your artifact small and fast to transfer.
- Save azure-pipelines.yml.
YAML indentation errors?
YAML is sensitive to whitespace. Make sure you use spaces (not tabs) for indentation, and each nested level adds exactly two spaces. If your editor mixed tabs and spaces, select all the content and re-indent.
help me fix YAML indentation in my azure-pipelines.yml
✔️ Awesome, I've got everything!
Great. Make sure you have saved azure-pipelines.yml before moving on.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file. Compare yours against it:
trigger:
branches:
include:
- main
pool:
name: 'my-local-agent'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: npm install
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
**/*
!node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy files to staging'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'node-app'
displayName: 'Publish artifact'
Commit and push the pipeline file
The pipeline file needs to be in Azure Repos for Azure DevOps to detect it. Pushing it now makes it available for when you register the pipeline later.
- Commit and push azure-pipelines.yml by running these commands:
git add azure-pipelines.yml
git commit -m "Add CI pipeline"
git push origin main
- Go back to Repos in your Azure DevOps project.
- You should see azure-pipelines.yml listed alongside your other files in the repository.
Push rejected or failed?
- If you see a rejection about the remote containing work you do not have locally, run git pull origin main first, then push again.
- Confirm you are on the main branch by running git branch. If you are on a different branch, switch with git checkout main.
help me push my pipeline file to Azure Repos
Your code and pipeline definition are now stored in Azure Repos. Next up, you will set up a self-hosted agent on your machine so the pipeline has somewhere to actually run.
Set Up a Self-Hosted Agent
Your pipeline YAML file is committed and ready to run. But a pipeline needs something to actually execute its jobs.
That something is called an agent. An agent is a machine that picks up pipeline jobs and runs them. In this step, you will set up a self-hosted agent on your own computer so your pipeline can execute immediately.
In this step, get ready to:
- Create a self-hosted agent pool in Azure DevOps.
- Generate a Personal Access Token for agent authentication.
- Download, configure, and run the agent on your machine.
Create an agent pool
An agent pool is a group of agents that your pipelines can target. Your azure-pipelines.yml already references a pool called my-local-agent. You need to create this pool in Azure DevOps so the pipeline knows where to find an agent.
- In your browser, go to your Azure DevOps organization at https://dev.azure.com/yourorgname.
- Click the gear icon (Organization settings) in the bottom-left corner of the sidebar.
- In the left menu under Pipelines, click Agent pools.
- Click Add pool.
- For Pool type, select Self-hosted.
- Enter my-local-agent as the pool name.
- Check the box for Grant access permission to all pipelines.
- Click Create.
What is an agent pool?
An agent pool is a collection of agents. When your pipeline runs, Azure DevOps looks at which pool the YAML file targets and assigns the job to an available agent in that pool.
By granting access to all pipelines, any pipeline in your project can use this pool without extra authorization steps.
Why a self-hosted agent instead of Microsoft-hosted?
Microsoft-hosted agents require a free parallelism grant that is temporarily disabled for new Azure DevOps organizations. New users must submit a request and wait several business days for approval.
A self-hosted agent runs on your own machine and gives you unlimited minutes at no cost. It also teaches you how real DevOps teams manage their own build infrastructure.
Generate a Personal Access Token (PAT)
The agent needs to authenticate with your Azure DevOps organization during its initial setup. A Personal Access Token (PAT) acts as an alternative password scoped to only the permissions the agent needs.
- In Azure DevOps, click the User settings icon (person with a gear) in the top-right corner.
- Select Personal access tokens.
- Click + New Token.
- In the Name field, enter agent-token.
- Set Expiration to 30 days.
- Under Scopes, click Show all scopes at the bottom of the dialog.
- Find Agent Pools and check Read & manage. Make sure all other boxes are cleared.
- Click Create.
- Copy the token immediately and save it somewhere safe (you cannot see it again after closing this dialog).
Save your PAT now
Azure DevOps only shows the PAT value once. If you close the dialog without copying it, you will need to delete the token and create a new one.
Why only Agent Pools scope?
The PAT is only used during the initial agent registration. It is not used for ongoing communication between the agent and Azure DevOps. Granting the minimum scope (Agent Pools Read & manage) follows the principle of least privilege.
Download and configure the agent
Now you will download the agent software to your machine, configure it to connect to your organization, and start it running. The process differs slightly between macOS and Windows.
- In Azure DevOps, go back to Organization settings > Agent pools.
- Click the my-local-agent pool you just created.
- Click the Agents tab, then click New agent.
- Select your operating system (macOS or Windows) in the dialog that appears.
- Click the Download button to download the agent package.
macOS
- Open Terminal and navigate to your Downloads folder by running:
cd ~/Downloads
- Create a folder for the agent and extract the downloaded package into it by running these commands:
mkdir ~/myagent && cd ~/myagent
tar xzf ~/Downloads/vsts-agent-osx-*.tar.gz
- Run the configuration script by running:
./config.sh
The script will ask you a series of questions. Answer them as follows:
- For Enter server URL, enter https://dev.azure.com/yourorgname (replace with your actual organization name).
- For Enter authentication type, press Enter to accept the default (PAT).
- For Enter personal access token, paste the PAT you copied earlier.
- For Enter agent pool, enter my-local-agent.
- For Enter agent name, press Enter to accept the default (your machine name), or type a custom name.
- For Enter work folder, press Enter to accept the default.
- Start the agent interactively by running:
./run.sh
You should see a message indicating the agent is listening for jobs. Keep this terminal window open.
Windows
- Create a folder at C:\agents and extract the downloaded ZIP file into it.
- Open an elevated PowerShell window (right-click PowerShell, select Run as administrator).
- Navigate to the agent folder by running:
cd C:\agents
- Run the configuration script:
.\config.cmd
The script will ask you a series of questions. Answer them as follows:
- For Enter server URL, enter https://dev.azure.com/yourorgname (replace with your actual organization name).
- For Enter authentication type, press Enter to accept the default (PAT).
- For Enter personal access token, paste the PAT you copied earlier.
- For Enter agent pool, enter my-local-agent.
- For Enter agent name, press Enter to accept the default (your machine name), or type a custom name.
- For Enter work folder, press Enter to accept the default.
- When asked about running as a service, enter N (you will run the agent interactively for this project).
- Start the agent interactively by running:
.\run.cmd
You should see a message indicating the agent is listening for jobs. Keep this PowerShell window open.
What does this configuration do?
The configuration script registers your machine as an agent in the my-local-agent pool. Once started, the agent polls Azure DevOps for queued jobs. When a pipeline targets this pool, the agent picks up the job and executes each step locally on your machine.
- Verify the agent is online by going back to Organization settings > Agent pools > my-local-agent > Agents tab in Azure DevOps.
- You should see your agent listed with a green Online status.
Agent not showing as online?
- Make sure the terminal or PowerShell window running the agent is still open. Closing it stops the agent.
- Confirm you entered the pool name as my-local-agent during configuration. If you used a different name, the agent registered to a different pool.
- If you see an authentication error, your PAT may have expired or been copied incorrectly. Create a new PAT and reconfigure the agent.
Still stuck? Help me troubleshoot why my self-hosted agent is not appearing online in Azure DevOps.
Your self-hosted agent is up and running. Next up, you will trigger the pipeline and watch it execute both stages on your machine.
Run the Pipeline and Add the Deploy Stage
Your self-hosted agent is running and ready to pick up work. Now it is time to connect the dots and see your CI/CD pipeline in action.
In this step, you will register your pipeline in Azure DevOps, watch the Build stage execute on your local agent, then extend the pipeline with a Deploy stage that packages and deploys your application.
In this step, get ready to:
- Register the pipeline in Azure DevOps and point it at your YAML file.
- Watch the Build stage execute and verify all steps pass.
- Add a Deploy stage that downloads the build artifact and deploys the application.
Register the pipeline in Azure DevOps
Azure DevOps needs to know which YAML file defines your pipeline. You will point it at the azure-pipelines.yml you already pushed to the main branch.
- In your Azure DevOps project, click Pipelines in the left sidebar.
- Click New pipeline.
- Select Azure Repos Git as your code source.
- Select your node-cicd-demo repository.
- On the Configure page, select Existing Azure Pipelines YAML file.
- Make sure the branch is set to main and select /azure-pipelines.yml as the path.
- Click Continue.
- Review the YAML preview, then click Run.
What just happened?
You told Azure DevOps where your pipeline definition lives. From now on, every push to the main branch will trigger a new pipeline run automatically.
Pipeline not starting?
Make sure your self-hosted agent is still running in your terminal. If you closed the terminal window, re-run the agent start command from Step 4.
Check that the pool name in your YAML (my-local-agent) matches the pool name you created in Organization Settings.
help me debug why my Azure DevOps pipeline is stuck and not running
Watch the Build stage execute
Once you click Run, Azure DevOps queues the job and your self-hosted agent picks it up. You can follow along in real time.
- Click on the running pipeline to open the run details.
- Click the Build and Test stage to expand it.
- Click BuildJob to see the live logs for each step.
You should see each step complete with a green checkmark: Install Node.js, Install dependencies, Run tests, Copy files to staging, and Publish artifact.
What should I see in the logs?
- Install Node.js downloads and sets up Node.js 18.x on your agent.
- Install dependencies runs npm install and you will see packages being downloaded.
- Run tests executes npm test and shows "2 passing" in the output.
- Copy files to staging and Publish artifact package your app as a build artifact called node-app.
Seeing a failed test step?
Check that your package.json has mocha test.js in the test script. If the test script is wrong, the pipeline will fail at that step.
Make sure you installed Chai version 4.x (not 5.x). Chai 5 requires ESM imports and will break the CommonJS setup in this project.
help me debug why my Azure DevOps pipeline tests are failing
Add the Deploy stage
A real CI/CD pipeline does not stop at building and testing. It also deploys the application. You will now add a second stage that downloads the build artifact and simulates a deployment.
- Open azure-pipelines.yml in your code editor.
- Find the end of the Build stage (after the Publish artifact task).
- Add the following Deploy stage below it:
- stage: Deploy
displayName: 'Deploy Application'
dependsOn: Build
jobs:
- job: DeployJob
displayName: 'Deploy to target'
steps:
- task: DownloadBuildArtifacts@1
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'node-app'
downloadPath: '$(System.ArtifactsDirectory)'
displayName: 'Download artifact'
- script: |
echo "Deploying application..."
echo "Artifact downloaded to: $(System.ArtifactsDirectory)/node-app"
echo "Deployment complete!"
displayName: 'Deploy application'
What does this Deploy stage do?
- dependsOn: Build ensures the Deploy stage only runs after the Build stage passes.
- DownloadBuildArtifacts@1 retrieves the node-app artifact that was published during the Build stage.
- The deploy script simulates a real deployment by echoing messages to the log. In a production pipeline, this would copy files to a server or push a container image.
- Save azure-pipelines.yml.
- Commit and push the update by running these commands:
git add azure-pipelines.yml
git commit -m "Add Deploy stage"
git push origin main
- Go back to Pipelines in Azure DevOps.
- Click on the new pipeline run that was triggered by your push.
- Watch both stages execute sequentially. The Build stage runs first, then the Deploy stage runs.
You should see two stages in the pipeline view, both with green checkmarks when complete.
Deploy stage failing?
Check your YAML indentation carefully. The - stage: Deploy line must be indented at the same level as - stage: Build (two spaces under stages:).
Make sure the artifact name node-app in the Deploy stage matches the artifact name in the Build stage's PublishBuildArtifacts@1 task.
help me debug why my Azure DevOps Deploy stage is failing
✔️ Awesome, I've got everything!
Great. Double check you have saved your file and both stages are passing in Azure DevOps.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml with both stages:
trigger:
branches:
include:
- main
pool:
name: 'my-local-agent'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: npm install
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
**/*
!node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy files to staging'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'node-app'
displayName: 'Publish artifact'
- stage: Deploy
displayName: 'Deploy Application'
dependsOn: Build
jobs:
- job: DeployJob
displayName: 'Deploy to target'
steps:
- task: DownloadBuildArtifacts@1
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'node-app'
downloadPath: '$(System.ArtifactsDirectory)'
displayName: 'Download artifact'
- script: |
echo "Deploying application..."
echo "Artifact downloaded to: $(System.ArtifactsDirectory)/node-app"
echo "Deployment complete!"
displayName: 'Deploy application'
You now have a fully automated two-stage pipeline. Every time you push code, it builds, tests, and deploys without any manual intervention. Next up, you will take on the Secret Mission and add a manual approval gate before deployment.
Secret mission
Add a Manual Approval Gate
In real-world teams, code does not go straight to production without a human sign-off. Add a manual approval gate to your pipeline so that the Deploy stage pauses and waits for explicit approval before running.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on Azure DevOps's free tier and your local machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Azure DevOps organization and project (node-cicd-demo).
- Self-hosted agent running on your machine.
- Personal Access Token (PAT) for agent authentication.
- Local project files (node-cicd-demo folder).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the pipeline soon.
- Your self-hosted agent continues listening for jobs. It uses minimal resources when idle.
- Your Azure DevOps organization and project remain available at no cost.
- Your PAT will expire after 30 days automatically.
Pause - I'll come back to this later
Shut down the running agent process to free up system resources. Your project and pipeline configuration stay intact in Azure DevOps.
- Stop the agent by pressing Ctrl+C in the terminal window where it is running.
- Close the terminal window.
- When you are ready to resume, navigate back to the agent folder and start it again with the run command.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow all steps below to fully clean up.
Remove the self-hosted agent
- Stop the agent by pressing Ctrl+C in the terminal where it is running.
- In the agent folder, run the remove command for your operating system:
macOS
./config.sh remove
Windows
.\config.cmd remove
- Enter your PAT when prompted to authenticate the removal.
Revoke the Personal Access Token
- In Azure DevOps, click the User Settings icon (person with gear, top right).
- Select Personal access tokens.
- Find agent-token, click the three dots, and select Revoke.
- Confirm the revocation.
Delete the Azure DevOps project
- In Azure DevOps, click Organization settings (gear icon, bottom left).
- Select Projects in the left sidebar.
- Find node-cicd-demo and click the three dots next to it.
- Select Delete.
- Type the project name to confirm and click Delete.
Delete the Azure DevOps organization (optional)
Only do this if you created the organization specifically for this project and do not plan to use Azure DevOps again.
- In Azure DevOps, click Organization settings.
- Select Overview.
- Click Delete.
- Enter the organization name and click Delete.
- You can recover a deleted organization within 28 days if you change your mind.
Delete local project files
- Delete the node-cicd-demo folder and the agent folder from your machine:
macOS
rm -rf ~/Desktop/node-cicd-demo
rm -rf ~/Desktop/myagent
Windows
Remove-Item -Recurse -Force ~\Desktop\node-cicd-demo
Remove-Item -Recurse -Force ~\Desktop\myagent
Nice Work!
Nice Work!
You just built a real CI/CD pipeline from scratch. Your code now flows from commit to deployment automatically, proving you understand how modern DevOps teams ship software continuously.
Here's what you accomplished:
- Created a multi-stage YAML pipeline in Azure DevOps that automatically builds, tests, and deploys a Node.js application.
- Configured a self-hosted agent running on your own machine to execute pipeline jobs with unlimited minutes.
- Pushed code and watched it flow from commit to deployment without any manual intervention.
- Added a manual approval gate to control production deployments, simulating a real team sign-off workflow.
Ready to quiz yourself?