Build a CI/CD Pipeline with Azure DevOps

Build a YAML CI/CD pipeline in Azure DevOps with a self-hosted agent.

Introduction

30 Second Summary

Imagine if every time you saved a document, a team of reviewers instantly checked it for errors, formatted it, and filed it away in the right place. That is what a CI/CD pipeline does for software.

In this project, you will build an automated pipeline that builds, tests, and packages a web application every time you push code. You will set up the entire system from scratch using Azure DevOps.

What You'll Build

A fully automated CI/CD pipeline in Azure DevOps that triggers on every code push to GitHub, installs dependencies, runs tests, and publishes a deployable artifact.

By the end of this project, you'll have:

  • A GitHub repository containing a Node.js application with automated tests.
  • A self-hosted build agent running on your Windows machine, connected to Azure Pipelines.
  • A YAML pipeline that automatically builds, tests, and packages your app on every commit to main.
  • Secret Mission: Add a multi-stage pipeline with separate Build and Deploy stages, including a manual approval gate before deployment.

Are there any prerequisites?

No prior experience with Azure DevOps or pipelines is needed. This project includes full setup instructions for every tool, starting from a fresh Windows machine.

Set Up Your Accounts and Tools

Before you can build a pipeline, you need the tools that power it. Git tracks your code changes, Node.js runs your application, and Azure DevOps orchestrates everything into an automated workflow.

In this step, you will install Git and Node.js on your Windows machine, create a GitHub repository to store your code, and set up an Azure DevOps project where your pipeline will live.

In this step, get ready to:
  • Install Git and Node.js on your Windows machine.
  • Create a GitHub repository and clone it locally.
  • Create an Azure DevOps organization and project.
Install Git and Node.js on your Windows machine

Your pipeline will use Git to pull code and Node.js to build and test it. You need both installed locally so you can develop and push code that triggers the pipeline.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • 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 Node.js check below.

ⓧ Command not found

Git is not installed yet. Install it by running this command in PowerShell:

winget install --id Git.Git -e --source winget
  • After the installation finishes, close and reopen PowerShell.
  • Run git --version again to confirm it works.

Alternative install method

If winget is not available, download the installer directly from https://git-scm.com/download/win and run it with the default settings.

Now check if Node.js is installed by running:

node --version

✔️ I see version 20 or higher

Node.js is ready. You should see something like v20.x.x or v22.x.x or higher. Move on to the next substep.

ⓧ I see a version lower than 20

Your Node.js version is too old for this project. The pipeline uses Node.js 20.x features including the built-in test runner.

  • Go to https://nodejs.org/en/download and download the LTS installer for Windows.
  • Run the installer and accept the default options.
  • Close and reopen PowerShell.
  • Run node --version again to confirm you see version 20 or higher.

ⓧ Command not found

Node.js is not installed yet.

  • Go to https://nodejs.org/en/download and download the LTS installer for Windows.
  • Run the installer and accept the default options.
  • Close and reopen PowerShell.
  • Run node --version again to confirm you see a version number starting with v20 or higher.

Why Node.js 20 or higher?

This project uses the built-in node:test module for running tests. This module became stable in Node.js 20. The Azure DevOps pipeline also installs Node.js 20.x on the build agent, so your local version should match.

Still having trouble installing?

Make sure you are running PowerShell as a regular user (not as Administrator) for the version checks. If winget commands fail, try closing PowerShell and reopening it, then retry.

help me troubleshoot installing Git or Node.js on Windows

Create a GitHub repository

Your pipeline needs a code repository to watch. Every time you push code to this repository, Azure DevOps will automatically trigger a build.

  • If you do not have a GitHub account, go to https://github.com and sign up for a free account.
  • Once signed in, click the + icon in the top-right corner and select New repository.
  • Enter azure-pipeline-demo as the repository name.
  • Leave visibility set to Public.
  • Check the box for Add a README file.
  • Click Create repository.

Now clone the repository to your machine so you can work on it locally.

  • On your new repository page, click the green Code button.
  • Make sure the HTTPS tab is selected, then click the copy icon to copy the URL.
  • Back in PowerShell, move to your Desktop by running this command:
cd ~/Desktop
  • Clone the repository by running:
git clone [[REPO_URL="your repository HTTPS URL from GitHub"]]

What does this command do?

The git clone command downloads a copy of your GitHub repository to your local machine. It creates a folder named azure-pipeline-demo on your Desktop containing the repository files.

  • Move into your new project folder by running:
cd azure-pipeline-demo
  • Confirm the clone worked by running:
ls

You should see a README.md file listed in the output.

Clone failed or permission denied?

Make sure you copied the HTTPS URL (not the SSH one) from GitHub. If you see a login prompt, enter your GitHub username and a personal access token (GitHub no longer accepts passwords for HTTPS Git operations).

help me fix a git clone error on Windows

Create your Azure DevOps project

Azure DevOps is where your pipeline lives. You will create an organization (your top-level container) and a project inside it to hold your pipeline configuration.

  • Open your browser and go to https://dev.azure.com.
  • Sign in with a Microsoft account (or create one if you do not have one).
  • Azure DevOps will prompt you to create an organization. Enter a name for your organization.
  • Select your closest region and click Continue.

What is an Azure DevOps organization?

An organization is the top-level container in Azure DevOps. It groups all your projects, pipelines, and users together under one account. Think of it like a company workspace.

Once your organization is created, you will land on a page to create your first project.

  • Enter PipelineDemo as the project name.
  • Set the visibility to Private.
  • Click Advanced to expand the additional settings.
  • Set Version control to Git.
  • Set Work item process to Agile.
  • Click Create project.

After a moment, you will land on the PipelineDemo project dashboard. This is where you will configure your pipeline in a later step.

Why self-hosted instead of Microsoft-hosted?

New Azure DevOps organizations do not automatically get free Microsoft-hosted parallel jobs. You would need to request a free grant that takes several business days to process.

A self-hosted agent runs on your own machine. It gives you immediate, unlimited build capacity at no cost. It also teaches you a valuable skill used in enterprise DevOps environments.

Stuck creating your organization?

Make sure you are signed into a personal Microsoft account (Outlook, Hotmail, or Live). Work or school accounts sometimes have restrictions that prevent creating new organizations.

help me troubleshoot creating an Azure DevOps organization

Your environment is ready. Next up, you will build a Node.js application with automated tests that your pipeline will run.

Build the Node.js Application

Your development environment is ready. Now you need an application for your pipeline to build, test, and package.

In this step, you will create a simple Node.js web server using Express, write automated tests for it, and push everything to GitHub.

In this step, get ready to:
  • Initialize a Node.js project and create an Express web server.
  • Write automated tests using Node.js built-in test runner.
  • Run tests locally, then commit and push your code to GitHub.
Initialize the project and create the server

Every Node.js project starts with a package.json file that tracks your project's metadata, dependencies, and scripts.

  • In PowerShell, make sure you are inside your cloned azure-pipeline-demo folder by running:
cd ~/Desktop/azure-pipeline-demo
  • Initialize your Node.js project by running:
npm init -y
  • Confirm the file was created by running:
ls

You should see package.json listed in the output.

Don't see package.json?

Make sure you ran npm init -y inside the azure-pipeline-demo folder, not a parent directory. Run pwd to check your current location.

Help me troubleshoot npm init

Now you need the application itself. Your server will respond to HTTP requests with a simple greeting message.

  • Create a new file called index.js in your azure-pipeline-demo folder and paste the following code:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

// Define the main route
app.get('/', (req, res) => {
  res.send('Hello from Azure Pipelines!');
});

// Only start the server when run directly (not imported)
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?

This creates a minimal web server using Express that responds with "Hello from Azure Pipelines!" when someone visits the root URL.

The if (require.main === module) check means the server only starts listening when you run the file directly. When another file imports it (like your tests), it skips starting the server. This lets you test the app without port conflicts.

  • Save index.js.

You won't see a change yet because Express is not installed. The next chunk handles that.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello from Azure Pipelines!');
});

if (require.main === module) {
  app.listen(port, () => {
    console.log(`Server running on port ${port}`);
  });
}

module.exports = app;
Install Express and write tests

Your server code references Express, but the package is not installed yet. You also need automated tests so the pipeline can verify your code works on every commit.

  • Install Express as a project dependency by running:
npm install express

You should see output confirming the package was added. A node_modules folder and package-lock.json file now appear in your project.

Seeing network or permission errors?

Check your internet connection. If you see EACCES errors, try running PowerShell as Administrator.

Help me troubleshoot npm install

Now you will write tests using the built-in Node.js test runner. This avoids installing a separate testing framework.

  • Create a new file called test.js in your azure-pipeline-demo folder and paste the following code:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const app = require('./index');

describe('App', () => {
  // Verify the app exports a function (Express apps are functions)
  it('should export an Express app', () => {
    assert.strictEqual(typeof app, 'function');
  });

  // Verify the server responds correctly on GET /
  it('should have a GET / route', async () => {
    const server = app.listen(0);
    const port = server.address().port;

    try {
      const response = await fetch(`http://localhost:${port}/`);
      const text = await response.text();
      assert.strictEqual(response.status, 200);
      assert.strictEqual(text, 'Hello from Azure Pipelines!');
    } finally {
      server.close();
    }
  });
});

What does this code do?

These tests verify two things about your application:

  • The first test checks that index.js exports a function (Express apps are callable functions).
  • The second test starts the server on a random port, makes an HTTP request, and confirms it returns status 200 with the expected text.

Using app.listen(0) picks a random available port so tests never conflict with a running server.

  • Save test.js.

You won't run the tests yet because the test script is not configured in package.json. The next substep handles that.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

const { describe, it } = require('node:test');
const assert = require('node:assert');
const app = require('./index');

describe('App', () => {
  it('should export an Express app', () => {
    assert.strictEqual(typeof app, 'function');
  });

  it('should have a GET / route', async () => {
    const server = app.listen(0);
    const port = server.address().port;

    try {
      const response = await fetch(`http://localhost:${port}/`);
      const text = await response.text();
      assert.strictEqual(response.status, 200);
      assert.strictEqual(text, 'Hello from Azure Pipelines!');
    } finally {
      server.close();
    }
  });
});
Configure scripts, test, and push to GitHub

Your pipeline will run scripts defined in package.json to build and test the app. Right now the default scripts section only has a placeholder. You need to replace it with real commands.

  • Open package.json in your editor.
  • Find the "scripts" section. It currently looks like this:
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
}
  • Replace the entire "scripts" block with the following:
"scripts": {
  "start": "node index.js",
  "build": "echo Build step completed",
  "test": "node --test test.js"
}
  • Also find the "description" line near the top. Change it from an empty string to:
"description": "Demo app for Azure DevOps CI/CD pipeline"

What do these scripts do?

  • start runs the server directly using Node.js.
  • build prints a confirmation message. In a real project this might compile TypeScript or bundle assets.
  • test uses the Node.js built-in test runner (node --test) to run test.js.
  • Save package.json.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

{
  "name": "azure-pipeline-demo",
  "version": "1.0.0",
  "description": "Demo app for Azure DevOps CI/CD pipeline",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "build": "echo Build step completed",
    "test": "node --test test.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.21.0"
  }
}

Now confirm your tests pass locally before pushing.

  • Run your tests by executing:
npm test

You should see output showing both tests passing with a summary like "tests 2" and "pass 2".

Tests failing?

If you see "Error: no test specified", you did not save the updated package.json scripts. Open the file and confirm the test script reads node --test test.js.

If you see a module not found error for Express, run npm install express again and ensure node_modules exists in your folder.

Help me figure out why my tests aren't passing

  • Commit and push all your files to GitHub by running these commands:
git add .
git commit -m "Add Node.js app with tests"
git push origin main

You should see output confirming the push to the main branch on GitHub.

Push rejected or authentication error?

If Git asks for credentials, make sure you are authenticated with GitHub. You may need to set up a personal access token or SSH key for pushing.

If the push is rejected because the remote has changes you do not have, run git pull origin main first, then push again.

Help me troubleshoot my git push

Your application is built, tested, and living on GitHub. Next, you will set up a self-hosted build agent so Azure DevOps can run your pipeline on your own machine.

Set Up the Self-Hosted Build Agent

Your Node.js application is built, tested, and pushed to GitHub. Now you need something to actually run your pipeline.

In Azure Pipelines, a build agent is a machine that executes your pipeline steps. You will set up a self-hosted agent on your own Windows computer, connect it to Azure DevOps, and watch it come online ready to accept jobs.

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 start the build agent on your machine.
Create the agent pool and generate a Personal Access Token

Before you can run a build agent, you need a pool for it to live in and a token so it can authenticate with your organization.

  • In your browser, go to your Azure DevOps organization at https://dev.azure.com/{your-organization}.
  • Click the gear icon in the bottom-left corner to open Organization settings.
  • In the left sidebar under Pipelines, click Agent pools.
  • Click Add pool at the top of the page.
  • Set Pool type to Self-hosted.
  • Enter SelfHosted 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 build agents that your pipelines can use. By creating a dedicated pool called SelfHosted, you are telling Azure DevOps exactly where to send pipeline jobs. Granting access to all pipelines means any pipeline in your project can use this pool without extra approvals.

Now generate a Personal Access Token (PAT) so your agent can authenticate with Azure DevOps.

  • Click your profile avatar in the top-right corner of Azure DevOps.
  • Select Personal access tokens from the dropdown menu.
  • Click + New Token.
  • Enter a name like AgentToken in the Name field.
  • Set the Expiration to 30 days (or any duration that covers your project).
  • Under Scopes, scroll down to Agent Pools and check Read & manage.
  • Click Create.
  • Copy the token immediately and save it somewhere safe (a text file or password manager). It will not be shown again.

What is a Personal Access Token?

A Personal Access Token (PAT) is a credential that lets tools authenticate with Azure DevOps on your behalf. The scope you selected limits this token to only managing agent pools. Treat it like a password.

Don't see the Agent Pools scope?

Click Show all scopes at the bottom of the scopes list. The Agent Pools option appears under the full list. If you still cannot create a PAT, your organization may have policies restricting token creation.

Help me troubleshoot PAT creation in Azure DevOps.

Download and configure the build agent

With the pool created and your PAT ready, you can now download and set up the agent software on your machine.

  • Back in Organization settings > Agent pools, click the SelfHosted pool you just created.
  • Click New agent at the top.
  • In the dialog that opens, select the Windows tab and click Download to get the x64 agent zip file.

Why self-hosted instead of Microsoft-hosted?

New Azure DevOps organizations do not automatically receive free Microsoft-hosted parallel jobs. Requesting the free grant takes several business days to process. A self-hosted agent gives you immediate, unlimited build capacity at no cost.

Self-hosted agents also teach a valuable skill. Many companies use them to run builds on machines with special hardware, proprietary software, or network access that cloud agents cannot provide.

  • Open File Explorer and create a new folder at C:\agents.
  • Extract the contents of the downloaded zip file into C:\agents.

Avoid spaces in the path

The agent path must not contain spaces. C:\agents is the recommended location from Microsoft's official documentation.

Now you will configure the agent using an interactive script.

  • Open PowerShell as Administrator by pressing the Windows key, typing PowerShell, then right-clicking Windows PowerShell and selecting Run as administrator.
  • Navigate to the agent folder by running this command:
cd C:\agents
  • Start the configuration script by running:
.\config.cmd

What will config.cmd ask?

The script walks you through several prompts. Here is what to enter at each one:

  • Enter server URL: Type your organization URL in the format https://dev.azure.com/{your-organization} and press Enter.
  • Enter authentication type: Press Enter to accept the default (PAT).
  • Enter personal access token: Paste the PAT you copied earlier and press Enter.
  • Enter agent pool: Type SelfHosted and press Enter.
  • Enter agent name: Press Enter to accept the default (your machine name).
  • Enter work folder: Press Enter to accept the default _work folder.

You should see a success message confirming the agent is configured.

Getting a connection error during config?

Double-check that your organization URL is correct. It should look like https://dev.azure.com/YourOrgName with no trailing slash. Also confirm that the PAT you pasted has not expired and has the Agent Pools (Read & manage) scope.

Help me fix an agent configuration connection error in Azure DevOps.

Start the agent and verify it is online

The agent is configured but not yet running. You need to start it so it can listen for pipeline jobs.

  • In the same elevated PowerShell window (still in C:\agents), start the agent by running:
.\run.cmd

You should see output indicating the agent is listening for jobs. The terminal will stay open and active.

What does run.cmd do?

This command starts the agent in interactive mode. The agent connects to Azure DevOps and waits for pipeline jobs. As long as this PowerShell window stays open, the agent is available. Closing the window stops the agent.

  • Go back to Azure DevOps in your browser.
  • Navigate to Organization settings > Agent pools > SelfHosted.
  • Click the Agents tab.
  • Verify your agent appears with a green Online status.

Agent showing as Offline?

Make sure the PowerShell window running .\run.cmd is still open. If you closed it, navigate back to C:\agents and run .\run.cmd again. Also verify your machine is connected to the internet.

Help me get my Azure DevOps self-hosted agent online.

Your self-hosted build agent is live and listening for work. Next up, you will write the YAML pipeline that tells this agent exactly what to do every time you push code.

Create the YAML Pipeline

Your self-hosted agent is online and waiting for instructions. Now you need to tell it exactly what to do when new code arrives.

You will write a YAML pipeline definition file that lives alongside your code in GitHub. This file describes every step your agent runs: install dependencies, build, test, and package an artifact.

In this step, get ready to:
  • Write a YAML pipeline file that builds and tests your Node.js app.
  • Add artifact publishing tasks that package your build output.
  • Create and run the pipeline in Azure DevOps.
Define the build and test pipeline

Azure DevOps reads a file called azure-pipelines.yml from the root of your repository to know how to build your project. This file specifies the trigger, the agent pool, and the steps to run.

  • In your code editor, create a new file called azure-pipelines.yml at the root of your azure-pipeline-demo folder (the same level as index.js and package.json).
  • Paste the following YAML into the file:
trigger:
  - main

pool: SelfHosted

steps:
  - task: UseNode@1
    inputs:
      version: '20.x'
    displayName: 'Install Node.js'

  - script: |
      npm install
    displayName: 'Install dependencies'

  - script: |
      npm run build
    displayName: 'Build project'

  - script: |
      npm test
    displayName: 'Run tests'

What does this YAML do?

  • trigger: - main tells Azure DevOps to run this pipeline every time someone pushes to the main branch.
  • pool: SelfHosted directs the pipeline to use the agent pool you created in the previous step.
  • UseNode@1 installs Node.js 20.x on the agent before running your code.
  • The three script steps run npm install, npm run build, and npm test in sequence.
  • Save azure-pipelines.yml.

You will not push this file yet. The next chunk adds artifact tasks that complete the pipeline.

Seeing red squiggles in the YAML?

YAML is very sensitive to indentation. Make sure you are using spaces (not tabs) and that each level is indented by exactly two spaces. Double-check that steps: is indented at the same level as trigger: and pool:.

help me fix YAML indentation errors in my azure-pipelines.yml file

Add artifact publishing tasks

A pipeline that builds and tests is useful, but professional pipelines also produce an artifact. An artifact is a packaged copy of your build output that you can deploy or download later.

  • In azure-pipelines.yml, find the last step (the Run tests block).
  • Add the following two tasks below it, at the same indentation level:
  - task: CopyFiles@2
    inputs:
      sourceFolder: '$(Build.SourcesDirectory)'
      contents: |
        index.js
        package.json
        package-lock.json
        test.js
      targetFolder: '$(Build.ArtifactStagingDirectory)'
    displayName: 'Copy project files'

  - task: PublishPipelineArtifact@1
    inputs:
      artifactName: 'nodejs-app'
      targetPath: '$(Build.ArtifactStagingDirectory)'
    displayName: 'Publish pipeline artifact'

What do these tasks do?

CopyFiles@2 copies your project files from the source directory into a staging area that Azure Pipelines uses to prepare artifacts.

PublishPipelineArtifact@1 takes everything in the staging area and publishes it as a downloadable artifact named nodejs-app. This is the packaged output of your pipeline run.

  • Save azure-pipelines.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved azure-pipelines.yml before moving on to the commit and push.

ⓧ 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: SelfHosted

steps:
  - task: UseNode@1
    inputs:
      version: '20.x'
    displayName: 'Install Node.js'

  - script: |
      npm install
    displayName: 'Install dependencies'

  - script: |
      npm run build
    displayName: 'Build project'

  - script: |
      npm test
    displayName: 'Run tests'

  - task: CopyFiles@2
    inputs:
      sourceFolder: '$(Build.SourcesDirectory)'
      contents: |
        index.js
        package.json
        package-lock.json
        test.js
      targetFolder: '$(Build.ArtifactStagingDirectory)'
    displayName: 'Copy project files'

  - task: PublishPipelineArtifact@1
    inputs:
      artifactName: 'nodejs-app'
      targetPath: '$(Build.ArtifactStagingDirectory)'
    displayName: 'Publish pipeline artifact'
  • Commit and push the new pipeline file to GitHub by running these commands in PowerShell:
git add azure-pipelines.yml
git commit -m "Add Azure Pipelines YAML configuration"
git push
  • You should see output confirming the push to origin/main.

Push rejected or authentication failed?

If Git asks for credentials, make sure you are signed into GitHub in your browser. For HTTPS repos, Git may prompt you to authenticate via a browser window.

If the push is rejected because of remote changes, run git pull --rebase first, then try pushing again.

help me fix a git push authentication or rejection error

Create and run the pipeline in Azure DevOps

Your YAML file is in GitHub. Now you need to tell Azure DevOps where to find it so it can start running your pipeline automatically.

  • In your Azure DevOps project PipelineDemo, click Pipelines in the left sidebar.
  • Click Create Pipeline (or New Pipeline).
  • On the Where is your code? screen, select GitHub.
  • If prompted, authorize the Azure Pipelines app to access your GitHub account. Follow the browser prompts to grant permission.
  • Select your azure-pipeline-demo repository from the list.
  • On the Configure your pipeline screen, select Existing Azure Pipelines YAML file.
  • In the dialog, make sure the branch is main and the path is /azure-pipelines.yml. Click Continue.
  • On the review screen, click Run to start your first pipeline run.

Why Existing Azure Pipelines YAML file?

Azure DevOps offers starter templates, but you already wrote your own azure-pipelines.yml. Choosing the existing file option tells Azure DevOps to use your committed YAML instead of generating a new one.

Azure DevOps picks up your pipeline and queues a run on your self-hosted agent. You should see the run appear under Pipelines with a status indicator (blue for running, green for success).

Pipeline stuck on 'Queued' or not starting?

Make sure your self-hosted agent is still running. Switch to the PowerShell window where you ran run.cmd and confirm it says Listening for Jobs.

Check that the pool name in your YAML (pool: SelfHosted) matches the pool name you created in Azure DevOps exactly, including capitalization.

help me figure out why my Azure DevOps pipeline is stuck in the queue and not running on my self-hosted agent

Your pipeline is now live. Next up, you will verify that every step passed and inspect the published artifact to confirm your automated build is producing real output.

Verify and Trigger CI

Your pipeline has already run once successfully. Now it is time to inspect the results and prove that the "continuous" part of Continuous Integration actually works.

In this step you will verify that every pipeline step passed, inspect the published artifact, and then trigger an automatic rebuild by pushing a code change.

In this step, get ready to:
  • Verify all pipeline steps passed in Azure DevOps.
  • Inspect the published build artifact.
  • Trigger automatic CI with a code change.
Verify the pipeline run

Every pipeline run in Azure DevOps shows a detailed breakdown of each step, including logs and pass/fail status. Checking this confirms your build infrastructure is healthy.

  • In your browser, navigate to your Azure DevOps project at dev.azure.com.
  • Click Pipelines in the left sidebar.
  • Click on your pipeline, then click on the most recent run (it should show a green checkmark).
  • Click on the job name to expand the step-by-step log view.

You should see each step listed with a green checkmark: Install Node.js, Install dependencies, Build project, Run tests, Copy project files, and Publish pipeline artifact.

What do these steps mean?

Each step maps directly to a task in your azure-pipelines.yml file. A green checkmark means the task exited with code 0 (success). If any step fails, the entire pipeline stops and reports an error.

Seeing a red X on any step?

Click the failed step to expand its logs. The error message at the bottom tells you what went wrong. Common causes include a typo in azure-pipelines.yml or the agent losing connection.

Make sure your agent is still running in the PowerShell window (you should see it listening for jobs).

Help me debug a failed pipeline step in Azure DevOps.

Inspect the published artifact

Your pipeline packages project files into a deployable artifact after every successful run. This artifact is what a deployment stage would pick up and push to a server.

  • From the completed pipeline run, click on the Summary tab.
  • In the Related section, click on the published artifact named nodejs-app.
  • Expand the artifact to view its contents.

You should see four files listed: index.js, package.json, package-lock.json, and test.js. These are the files your CopyFiles@2 task selected.

What is a pipeline artifact?

A pipeline artifact is a packaged snapshot of your build output. In a real production pipeline, a later deployment stage would download this artifact and deploy it to a server or cloud service.

By publishing artifacts, you decouple building from deploying. You can redeploy the same tested artifact to multiple environments without rebuilding.

Trigger CI with a code change

The real power of CI is that it runs automatically on every push. To prove this, you will make a small change to your app code and watch the pipeline fire without any manual intervention.

  • In your code editor, open index.js inside your azure-pipeline-demo folder.
  • Find this line:
  res.send('Hello from Azure Pipelines!');
  • Replace 'Hello from Azure Pipelines!' with a new message, so the line looks like this:
  res.send('Hello from Azure Pipelines v2!');
  • Now open test.js and find this assertion line:
      assert.strictEqual(text, 'Hello from Azure Pipelines!');
  • Update it to match your new message:
      assert.strictEqual(text, 'Hello from Azure Pipelines v2!');

Why update the test too?

Your test asserts the exact response text from the server. If you change the message in index.js without updating test.js, the test will fail and so will your pipeline. Keeping tests in sync with code is a core CI discipline.

  • Save both files.
  • Commit and push your changes by running these commands in PowerShell:
git add .
git commit -m "Update response message to v2"
git push
  • Switch back to Azure DevOps in your browser.
  • Click Pipelines in the left sidebar.
  • Watch for a new pipeline run to appear automatically (it may take 10-30 seconds to trigger).

You should see a new run kick off without you clicking anything. Once it completes with a green checkmark, you have confirmed that Continuous Integration is fully working.

What just happened?

When you pushed to the main branch, GitHub notified Azure DevOps via a webhook. Azure DevOps matched the push against your pipeline's trigger: - main rule and queued a new run on your self-hosted agent.

This is the CI loop: push code, pipeline triggers, tests run, artifact publishes. Every future commit follows the same path automatically.

Pipeline not triggering automatically?

Make sure you pushed to the main branch (not a feature branch). Your trigger is set to main only.

Confirm your self-hosted agent is still running in the PowerShell window. If the agent stopped, navigate to C:\agents and run .\run.cmd again.

Help me figure out why my Azure DevOps pipeline is not triggering automatically after a push.

Your pipeline is now fully operational. Every commit to main triggers an automatic build, test, and artifact publish cycle. This is the foundation of professional software delivery.

Secret mission

Add a Multi-Stage Pipeline with Approval Gate

Your pipeline builds and tests on every push. But in the real world, you would never deploy to production without someone reviewing and approving the release first. Can you restructure your pipeline into separate Build and Deploy stages, with a manual approval gate that pauses the deployment until you say go?

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 uses the Azure DevOps free tier with a self-hosted agent, so there are no ongoing cloud costs.

Resources you used:

  • Self-hosted agent running interactively at C:\agents.
  • Azure DevOps organization, project (PipelineDemo), agent pool (SelfHosted), and environment (Production).
  • Personal Access Token (PAT) with Agent Pools scope.
  • GitHub repository (azure-pipeline-demo).
  • Local project files on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building on your pipeline or plan to extend it soon.

  • Your self-hosted agent continues to listen for pipeline jobs in the PowerShell window running run.cmd.
  • Your Azure DevOps project and GitHub repository remain available for future pipeline runs.
  • Your PAT remains active until its expiry date.
  • Git and Node.js remain installed on your machine as general-purpose development tools.

Pause - I'll come back to this later

Shut down running processes to free up system resources. Your Azure DevOps project and GitHub repo stay intact for when you return.

  • In the PowerShell window where your agent is running, press Ctrl+C to stop the agent.
  • Close the PowerShell window.
  • When you are ready to resume, open an elevated PowerShell window, navigate to C:\agents, and run .\run.cmd again.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to clean up completely.

Stop and remove the self-hosted agent:

  • In the PowerShell window where your agent is running, press Ctrl+C to stop it.
  • In the same elevated PowerShell window, navigate to the agent folder and remove the agent configuration by running:
cd C:\agents
.\config remove
  • When prompted, enter your PAT to authenticate the removal.
  • After removal completes, delete the agent folder by running:
Remove-Item -Recurse -Force C:\agents

Revoke your Personal Access Token:

  • In Azure DevOps, click your profile icon in the top right corner.
  • Select Personal access tokens under Security.
  • Find the token you created for this project, click it, and select Revoke.
  • Confirm the revocation in the dialog.

Delete the Azure DevOps project:

  • In Azure DevOps, click Organization settings (gear icon in the bottom-left sidebar).
  • Select Projects in the left menu.
  • Find PipelineDemo, click the ellipsis (...) next to it, and select Delete.
  • Confirm by typing the project name and clicking Delete.

Delete the Azure DevOps organization (optional):

  • In Organization settings, select Overview.
  • Click Delete.
  • Enter the organization name and confirm.

Delete the GitHub repository:

  • Go to your azure-pipeline-demo repository on GitHub.
  • Click Settings (top tab bar).
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Follow the confirmation prompts to complete deletion.

Delete local project files:

  • In PowerShell, remove the cloned repository folder by running:
Remove-Item -Recurse -Force ~\Desktop\azure-pipeline-demo

Nice Work!

Nice Work!

You just built a production-style CI/CD pipeline from scratch using Azure DevOps. Every time you push code to GitHub, your pipeline automatically builds, tests, and packages your application without any manual intervention.

Here's what you accomplished:

  • Configured a self-hosted build agent on your Windows machine, connected to Azure DevOps for unlimited build capacity at no cost.
  • Wrote a YAML pipeline that automatically installs dependencies, builds, tests, and publishes your Node.js application on every commit to main.
  • Published a pipeline artifact (nodejs-app) that packages your code into a deployable unit after every successful run.
  • Added multi-stage pipelines with a manual approval gate for controlled production deployments.

Ready to quiz yourself?