Build a CI/CD Pipeline with Azure DevOps
Build a multi-stage YAML CI/CD pipeline that auto-builds and tests a Node.js app.
Introduction
30 Second Summary
Every time you save code and manually test it before sharing with your team, you're doing what professional developers automate with CI/CD pipelines. These pipelines catch bugs, run tests, and package your app automatically so nothing broken reaches production.
In this project, you will build a multi-stage CI/CD pipeline in Azure DevOps that automatically builds, tests, and packages a Node.js application every time you push code to GitHub.
What You'll Build
A YAML-based pipeline in Azure DevOps that triggers on every code push, runs automated tests, and publishes deployable artifacts.
By the end of this project, you'll have:
- A Node.js Express application connected to an Azure DevOps pipeline via GitHub.
- A multi-stage YAML pipeline that installs dependencies, runs automated tests, and produces build artifacts.
- A published pipeline artifact ready for deployment to any hosting environment, plus a status badge showing pipeline health on your repository.
- Secret Mission: Add a Pull Request validation pipeline that blocks merges unless all tests pass.
Are there any prerequisites?
You should have a basic understanding of Git (commits, push, pull). You will also need a Microsoft account or GitHub account for signing up to Azure DevOps.
Before We Start
Before We Start...
Set Up Your Accounts and Tools
Every CI/CD pipeline needs a home. Azure DevOps is a cloud platform where you create, run, and monitor pipelines that automatically build and test your code.
In this step, you'll create your Azure DevOps workspace and verify that Git and Node.js are installed on your machine.
In this step, get ready to:
- Create a free Azure DevOps organization and project.
- Verify that Git and Node.js are installed on your machine.
- Request a free parallel job grant if needed.
Create a free Azure DevOps organization
Azure DevOps organizes your work into organizations and projects. An organization is the top-level container that holds all your projects.
- Open your browser and go to dev.azure.com.
- Select Start free.
- Sign in with your Microsoft account or GitHub account.
- When prompted to create a new organization, enter a unique name (for example, yourname-devops).
- Choose a hosting region near you.
- Select Continue to finish creating your organization.
After creating your organization, Azure DevOps takes you to your new organization's home page.
You may need to request a free parallel job grant
New Azure DevOps organizations sometimes start with 0 Microsoft-hosted parallel jobs. This means your pipeline cannot run until the grant is approved.
To check, navigate to your organization's settings:
- Select Organization Settings in the bottom-left corner.
- Select Pipelines, then Parallel jobs.
- If you see 0 Microsoft-hosted jobs, select Request free grant. Approval typically takes 2-3 business days.
Alternatively, you can use a self-hosted agent to run your pipeline immediately without waiting for the grant.
What is an Azure DevOps organization?
An organization is the top-level container in Azure DevOps. It groups related projects together and controls billing, users, and security at a high level.
Your organization lives at https://dev.azure.com/your-org-name. You can have multiple projects inside one organization.
Create an Azure DevOps project
A project is where your pipeline, code repositories, and work items live. You need one project to hold the CI/CD pipeline you are building.
- On your organization home page, select New project in the top-right corner.
- Enter nodejs-cicd-pipeline as the project name.
- Set the visibility to Private.
- Leave the other settings at their defaults.
- Select Create.
Azure DevOps displays your new project's welcome page. You should see the project name nodejs-cicd-pipeline at the top of the page.
Don't see the New project button?
Make sure you are on your organization's home page at dev.azure.com/your-org-name. The button only appears if you have the correct permissions. As the organization creator, you have these permissions by default.
help me find the New project button in Azure DevOps
Verify Git and Node.js are installed
Your local machine needs Git (to push code) and Node.js (to run and test the application). Let's confirm both are installed and at the right versions.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
First, check that Git is installed by running this command:
- Check your Git installation by running this command:
git --version
✔️ I see a version number
Git is installed and ready to use. Move on to the Node.js check below.
ⓧ Command not found
Git is not installed yet. Install it from the official site:
- Go to git-scm.com and download the installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run git --version again to confirm.
Next, check that Node.js is installed at version 18 or higher.
- Check your Node.js version by running this command:
node --version
✔️ I see version 18 or higher
Node.js is ready. You should see something like v18.x.x, v20.x.x, or v24.x.x. Any version at 18 or above works for this project.
ⓧ I see a version lower than 18
Your Node.js version is too old for this project. You need version 18 or later.
- Go to nodejs.org and download the LTS version.
- Run the installer. It will replace your current version with the latest LTS.
- Close and reopen your terminal, then run node --version again to confirm you see v18 or higher.
Why does the old version persist?
On macOS, the system may have a default Node.js version that takes priority. If after installing the LTS version you still see an old version, try running which node to see which binary your terminal resolves. You may need to update your PATH or use a version manager like nvm.
ⓧ Command not found
Node.js is not installed yet. Install the LTS version:
- Go to nodejs.org and download the LTS installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run node --version again to confirm you see v18 or higher.
Still having trouble with Git or Node.js?
Make sure you closed and reopened your terminal after installing. New installations are only recognized in fresh terminal sessions.
help me install Git and Node.js for my Azure DevOps CI/CD pipeline project
Your accounts and tools are ready. Next up, you'll create a Node.js application and push it to GitHub so your pipeline has code to build and test.
Create and Push a Node.js App to GitHub
Your Azure DevOps project is set up and ready to run pipelines. Now you need an application for it to build and test.
In this step, you'll create a simple Node.js Express app with automated tests and push it to a GitHub repository. This is the codebase your pipeline will monitor for changes.
In this step, get ready to:
- Create a GitHub repository for your application code.
- Build a Node.js Express app with a health endpoint and automated tests.
- Push your code to GitHub so Azure DevOps can access it.
Create a new GitHub repository
Your pipeline needs a code source to watch. GitHub is the most common place to host code that connects to Azure DevOps Pipelines.
- Go to github.com and sign in (or create a free account if you don't have one).
- Click the + icon in the top-right corner of the page.
- Select New repository.
- Enter nodejs-pipeline-app as the repository name.
- Leave the visibility set to Public (or choose Private if you prefer).
- Do NOT check Add a README file. Leave all initialization options unchecked.
- Click Create repository.
GitHub shows a Quick Setup page with your repository URL. Keep this page open.
Why leave the repository empty?
You are about to push code from your local machine. If GitHub initializes the repository with a README, you would have to merge histories before pushing. Starting empty avoids that extra step.
Set up the Node.js project locally
You'll build a small Express web server with two endpoints and a test file. This gives the pipeline something meaningful to install, test, and package.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and initialize it by running these commands:
mkdir nodejs-pipeline-app
cd nodejs-pipeline-app
npm init -y
npm install express
npm install --save-dev jest
What do these commands do?
- mkdir nodejs-pipeline-app creates a new folder for your project.
- npm init -y generates a package.json with default values.
- npm install express installs the Express web framework as a dependency.
- npm install --save-dev jest installs Jest (a testing framework) as a development dependency.
- Confirm the project was created by running:
ls
You should see node_modules and package.json listed in the output.
Seeing 'npm: command not found'?
This means Node.js is not installed correctly. Go back to the previous step and verify your Node.js installation with node --version.
help me fix npm not being recognized in my terminal
- Open the nodejs-pipeline-app folder in VS Code by running:
code .
Use your preferred editor
If you use Cursor or another code editor, open the nodejs-pipeline-app folder from that editor's File menu instead.
Now you'll update package.json to add the correct scripts, then create your application and test files.
- In VS Code, click on package.json in the Explorer sidebar to open it.
- Replace the entire contents of package.json with the following:
{
"name": "nodejs-pipeline-app",
"version": "1.0.0",
"description": "A Node.js app for learning Azure DevOps CI/CD pipelines",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "jest"
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
What changed in package.json?
- The "test": "jest" script tells npm test to run Jest. Your pipeline will use this exact command to run automated tests.
- The "start": "node app.js" script provides a standard way to launch the app.
- Save package.json with Cmd+S (macOS) or Ctrl+S (Windows).
Next, create the main application file.
- In the Explorer sidebar, right-click in the empty area below the file list and select New File.
- Name the file app.js and press Enter.
- Paste the following code into app.js:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// Root endpoint returns a JSON greeting
app.get('/', (req, res) => {
res.json({ message: 'Hello from the CI/CD Pipeline!' });
});
// Health check endpoint for monitoring
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(`App listening on port ${port}`);
});
}
// Export for testing
module.exports = app;
What does this code do?
- The / route returns a JSON message. This is the main endpoint your app serves.
- The /health route returns a status and timestamp. Production apps use health endpoints for monitoring.
- The if (require.main === module) guard means the server only starts when you run the file directly. When Jest imports it for testing, it skips starting the server.
- module.exports = app makes the app available to your test file.
- Save app.js.
Now create the test file that your pipeline will run.
- Right-click in the Explorer sidebar again and select New File.
- Name the file app.test.js and press Enter.
- Paste the following code into app.test.js:
const app = require('./app');
describe('App', () => {
// Verify the app module loads correctly
test('should export the app module', () => {
expect(app).toBeDefined();
});
// Verify the root route is registered
test('should have a GET / route', () => {
const routes = app._router.stack
.filter(layer => layer.route)
.map(layer => ({
path: layer.route.path,
method: Object.keys(layer.route.methods)[0]
}));
const rootRoute = routes.find(r => r.path === '/' && r.method === 'get');
expect(rootRoute).toBeDefined();
});
// Verify the health endpoint is registered
test('should have a GET /health route', () => {
const routes = app._router.stack
.filter(layer => layer.route)
.map(layer => ({
path: layer.route.path,
method: Object.keys(layer.route.methods)[0]
}));
const healthRoute = routes.find(r => r.path === '/health' && r.method === 'get');
expect(healthRoute).toBeDefined();
});
});
What do these tests check?
- The first test confirms the app module loads without errors.
- The second test checks that a GET route exists at /.
- The third test checks that a GET route exists at /health. These tests will run automatically in your pipeline on every commit.
- Save app.test.js.
- Verify your tests pass by running this in the terminal:
npm test
You should see output showing 3 passing tests with green checkmarks.
Tests failing or command not working?
- If you see a "no test specified" error, make sure you saved the updated package.json with the "test": "jest" script.
- If you see "Cannot find module './app'", confirm app.js is saved in the same folder as app.test.js.
- Make sure you are running the command from inside the nodejs-pipeline-app folder.
help me debug my failing Jest tests in my Node.js project
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here are the three files you should have in your nodejs-pipeline-app folder:
{
"name": "nodejs-pipeline-app",
"version": "1.0.0",
"description": "A Node.js app for learning Azure DevOps CI/CD pipelines",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "jest"
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
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(`App listening on port ${port}`);
});
}
module.exports = app;
const app = require('./app');
describe('App', () => {
test('should export the app module', () => {
expect(app).toBeDefined();
});
test('should have a GET / route', () => {
const routes = app._router.stack
.filter(layer => layer.route)
.map(layer => ({
path: layer.route.path,
method: Object.keys(layer.route.methods)[0]
}));
const rootRoute = routes.find(r => r.path === '/' && r.method === 'get');
expect(rootRoute).toBeDefined();
});
test('should have a GET /health route', () => {
const routes = app._router.stack
.filter(layer => layer.route)
.map(layer => ({
path: layer.route.path,
method: Object.keys(layer.route.methods)[0]
}));
const healthRoute = routes.find(r => r.path === '/health' && r.method === 'get');
expect(healthRoute).toBeDefined();
});
});
Push the code to GitHub
Your app is ready locally. Now push it to the GitHub repository so Azure DevOps can access it in the next step.
- Initialize Git and push your code by running these commands:
git init
git add .
git commit -m "Initial commit: Node.js app with tests"
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/nodejs-pipeline-app.git
git push -u origin main
What do these commands do?
- git init turns the folder into a Git repository.
- git add . stages all your files for the first commit.
- git branch -M main renames the default branch to main.
- git remote add origin connects your local repository to the GitHub repository.
- git push -u origin main uploads your code to GitHub.
Git may prompt you to sign in to GitHub. Follow the authentication prompts in your terminal or browser to authorize access.
- Refresh your GitHub repository page in the browser.
- You should see app.js, app.test.js, and package.json listed in the repository.
Push rejected or authentication error?
- If you see "remote: Repository not found", double-check that the URL matches your GitHub username and repository name exactly.
- If authentication fails, GitHub requires a personal access token or SSH key. Visit your GitHub Settings > Developer settings > Personal access tokens to generate one.
- If the branch already exists with different history, make sure you did NOT initialize the repository with a README on GitHub.
help me push my code to GitHub from the terminal
Your application is live on GitHub. Next up, you'll connect this repository to Azure DevOps and create your first CI/CD pipeline.
Create Your First YAML Pipeline
Your Node.js application is on GitHub with working tests. Now it is time to automate those tests so they run every time you push code.
In this step, you will create a YAML pipeline in Azure DevOps that connects to your GitHub repository, installs dependencies, and runs your tests automatically.
In this step, get ready to:
- Connect Azure DevOps Pipelines to your GitHub repository.
- Write a YAML pipeline definition with a Build stage.
- Run your first automated pipeline and watch it pass.
Connect Azure DevOps to your GitHub repository
Azure Pipelines needs permission to read your GitHub repository so it can pull your code and run builds against it.
- In your Azure DevOps project, select Pipelines from the left sidebar.
- Select Create Pipeline (or New pipeline if you see that option instead).
- On the "Where is your code?" screen, select GitHub.
- A GitHub authorization window appears. Sign in with your GitHub credentials and authorize Azure Pipelines to access your repositories.
- Select your nodejs-pipeline-app repository from the list.
What is happening during authorization?
When you authorize, Azure Pipelines installs a GitHub App or creates an OAuth connection to your account. This lets Azure DevOps pull code from your repository and report build status back to GitHub.
Don't see your repository in the list?
Make sure you are signed into the correct GitHub account. The repository must be owned by the account you authorized. If you just created the repository, try refreshing the page.
help me troubleshoot why my GitHub repository is not showing up in Azure DevOps
Configure the pipeline with a Starter template
Azure DevOps offers several templates to get you started. You will begin with the Starter pipeline and replace its contents with your own custom YAML.
- On the "Configure your pipeline" screen, select Starter pipeline.
- Azure DevOps opens a YAML editor pre-filled with a basic template.
- Select all the existing content in the editor and delete it.
- Paste the following YAML pipeline definition into the editor:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '18.x'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: UseNode@1
inputs:
version: $(nodeVersion)
displayName: 'Install Node.js'
- script: npm install
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
What does this pipeline do?
This YAML file defines your entire CI workflow as code. Here is what each section means:
- trigger: - main tells Azure Pipelines to run this pipeline every time a change is pushed to the main branch.
- vmImage: 'ubuntu-latest' selects a Microsoft-hosted Linux agent to run your build.
- UseNode@1 installs Node.js 18.x on the build agent.
- npm install downloads your project dependencies (Express and Jest).
- npm test runs your Jest test suite. If any test fails, the pipeline fails too.
Why YAML instead of a visual editor?
Azure DevOps supports both YAML and Classic (visual) pipelines. YAML pipelines store your build definition as code inside your repository. This means your pipeline is version-controlled, reviewable in pull requests, and portable across projects.
Microsoft has invested all new pipeline features exclusively in the YAML model. Classic pipelines are in maintenance mode.
Save and run the pipeline
Saving the pipeline commits the azure-pipelines.yml file directly to your GitHub repository and triggers the first run.
- Select Save and run in the top-right corner of the editor.
- A dialog appears asking for a commit message. Keep the default message or enter your own.
- Select Save and run again to confirm.
- Azure DevOps commits azure-pipelines.yml to the main branch of your GitHub repository and immediately starts the pipeline.
- Watch the pipeline run on the Summary page. You should see the Build stage turn green after about a minute, meaning all tests passed.
What should I see?
The run Summary page shows your pipeline with one stage called "Build and Test". Inside it, the job "Install, Build, and Test" shows three completed steps: Install Node.js, Install dependencies, and Run tests. All should have green checkmarks.
Pipeline stuck or showing 0 parallel jobs?
New Azure DevOps organizations may need to request a free grant for Microsoft-hosted parallel jobs. Navigate to Organization Settings then Pipelines then Parallel jobs. If you see 0 Microsoft-hosted jobs, select Request free grant. Approval typically takes 2-3 business days.
help me figure out why my Azure DevOps pipeline is not running
✔️ Awesome, I've got everything!
Great. Double check that your azure-pipelines.yml file was saved and your pipeline run completed successfully.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml that should now exist in your repository:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '18.x'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: UseNode@1
inputs:
version: $(nodeVersion)
displayName: 'Install Node.js'
- script: npm install
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
Your pipeline is alive. Every push to main now triggers an automated build and test. Next up, you will add a second stage that packages your application into a deployable artifact.
Add a Package Stage and Pipeline Artifacts
Your pipeline successfully builds and tests your application on every commit. That's the Continuous Integration half of CI/CD.
Now it's time to add the Continuous Delivery half. You'll create a second pipeline stage that packages your tested code into a deployable artifact. This artifact is what a team would hand off to a production server.
In this step, get ready to:
- Add a Package stage to your pipeline YAML.
- Run the updated pipeline and verify both stages pass.
- Download and inspect the published artifact.
Edit the pipeline to add a Package stage
A pipeline with multiple stages models a real delivery workflow. Each stage runs in its own fresh environment, so the Package stage proves your application can be installed and bundled from scratch.
- In your Azure DevOps project, select Pipelines from the left sidebar.
- Select your pipeline from the list.
- Select Edit to open the YAML editor.
You should see the existing Build stage in the editor. You'll add the Package stage directly below it.
- Scroll to the bottom of the file (after the last line of the Build stage) and paste the following YAML:
- stage: Package
displayName: 'Package Artifact'
dependsOn: Build
condition: succeeded()
jobs:
- job: PackageJob
displayName: 'Create and Publish Artifact'
steps:
- task: UseNode@1
inputs:
version: $(nodeVersion)
displayName: 'Install Node.js'
- script: npm install --production
displayName: 'Install production dependencies'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
app.js
package.json
node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy files to staging'
- task: PublishPipelineArtifact@1
inputs:
artifactName: 'nodejs-app'
targetPath: '$(Build.ArtifactStagingDirectory)'
displayName: 'Publish pipeline artifact'
What does this code do?
- dependsOn: Build tells Azure Pipelines to only run the Package stage after the Build stage passes. condition: succeeded() adds an extra check that the previous stage actually succeeded.
- npm install --production installs only the packages your app needs to run (skipping test tools like Jest). This keeps the artifact lightweight.
- CopyFiles@2 gathers app.js, package.json, and node_modules into a staging directory that Azure Pipelines manages.
- PublishPipelineArtifact@1 takes the staged files and publishes them as a named artifact called nodejs-app. This artifact is downloadable from the pipeline run summary.
What is a pipeline artifact?
A pipeline artifact is a bundle of files produced during a pipeline run. Think of it as a zip file that contains everything needed to deploy your application. In a full production workflow, a deployment stage would pick up this artifact and push it to a live server.
Make sure the Package stage is indented at the same level as the Build stage (both are items under stages:).
✔️ Awesome, I've got everything!
Great. Double check your indentation is correct before moving on.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file with both stages:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '18.x'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Install, Build, and Test'
steps:
- task: UseNode@1
inputs:
version: $(nodeVersion)
displayName: 'Install Node.js'
- script: npm install
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
- stage: Package
displayName: 'Package Artifact'
dependsOn: Build
condition: succeeded()
jobs:
- job: PackageJob
displayName: 'Create and Publish Artifact'
steps:
- task: UseNode@1
inputs:
version: $(nodeVersion)
displayName: 'Install Node.js'
- script: npm install --production
displayName: 'Install production dependencies'
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: |
app.js
package.json
node_modules/**
targetFolder: '$(Build.ArtifactStagingDirectory)'
displayName: 'Copy files to staging'
- task: PublishPipelineArtifact@1
inputs:
artifactName: 'nodejs-app'
targetPath: '$(Build.ArtifactStagingDirectory)'
displayName: 'Publish pipeline artifact'
Save button greyed out?
If you cannot save, there is likely a YAML syntax error. Check that your indentation uses spaces (not tabs) and that the Package stage lines up with the Build stage under stages:.
Help me fix a YAML indentation error in my Azure DevOps pipeline.
Run the updated pipeline and verify artifacts
- Select Save and run in the top right corner of the editor.
- In the dialog that appears, select Save and run again to commit the change to your repository.
Azure DevOps commits the updated azure-pipelines.yml to your main branch and immediately triggers a new pipeline run.
- Watch the run execute on the Summary page. You should see two stages: Build and Test followed by Package Artifact.
- Wait for both stages to show green checkmarks indicating success.
- On the run Summary page, scroll down to confirm a published artifact named nodejs-app appears in the Related section.
Package stage failed?
Select the failed stage to view the job logs. Look for red error text in the step output. Common causes include a typo in sourceFolder or targetFolder paths, or a missing dependsOn: Build line.
Help me debug a failed Package stage in my Azure DevOps pipeline.
Download and inspect the artifact
The published artifact is what a deployment stage would pick up in a full production pipeline. Inspecting it confirms your pipeline is packaging the right files.
- On the pipeline run Summary page, find the nodejs-app artifact link in the Related section.
- Select the artifact name to browse its contents.
- Verify that you see app.js, package.json, and a node_modules folder listed inside the artifact.
Why only production dependencies?
The Package stage runs npm install --production instead of npm install. This skips devDependencies like Jest, keeping your artifact smaller. Test tools already did their job in the Build stage.
Your pipeline now builds, tests, and packages your application automatically. Next, you'll trigger a CI build with a real code change and add a status badge to your repository.
Trigger a CI Build and Add a Status Badge
Your pipeline now builds, tests, and packages your application into a publishable artifact. But right now, you have to manually click "Save and run" every time you want to test your code.
The real power of CI/CD is automation. In this step, you will prove that your pipeline triggers automatically on every push and add a visible status badge so anyone visiting your repository can see if the build is healthy.
In this step, get ready to:
- Trigger an automatic CI build by pushing a code change.
- Add a pipeline status badge to your repository README.
- Verify the end-to-end CI flow works on every commit.
Trigger a CI build with a code change
Your azure-pipelines.yml already has a trigger section set to main. This means any push to the main branch automatically starts the pipeline. Let's prove it works.
- Switch back to VS Code with your nodejs-pipeline-app project open.
- Open app.js from the Explorer sidebar.
- Add a new comment line above the /health route. For example, type // Health check endpoint for monitoring on a blank line above app.get('/health'.
- Save app.js.
Now commit and push this change to GitHub so the pipeline can detect it.
- Stage, commit, and push the change by running these commands in your terminal:
git add .
git commit -m "Add comment to health endpoint"
git push
- Return to Azure DevOps in your browser.
- Select Pipelines from the left menu.
- You should see a new pipeline run that was triggered automatically. The run's commit message will show "Add comment to health endpoint".
What just happened?
You didn't click anything in Azure DevOps to start this run. The trigger: - main line in your YAML told Azure Pipelines to watch for new commits on the main branch. The moment you pushed, the pipeline kicked off.
This is Continuous Integration in action. Every change is automatically validated.
Pipeline didn't trigger?
Make sure you pushed to the main branch specifically. If you're on a different branch, the trigger won't fire.
Also confirm the push succeeded by checking your GitHub repository. Your latest commit should appear at the top of the file list.
help me figure out why my Azure DevOps pipeline didn't trigger on push
Add a status badge to your README
A status badge is a small image that shows whether your pipeline is currently passing or failing. Adding one to your repository gives instant visibility into the health of your project.
- In Azure DevOps, select Pipelines from the left menu.
- Select your pipeline from the list.
- Click the three-dot menu (...) in the top-right area of the pipeline page.
- Select Status badge.
- In the dialog that appears, copy the Sample Markdown text. This is the badge code you will paste into your README.
What is the badge markdown?
The copied markdown is a special image link that points to Azure DevOps. It dynamically updates to show green (passing) or red (failing) based on your latest pipeline run.
- Create a new file called README.md in the root of your nodejs-pipeline-app folder by running:
touch README.md
- Open README.md in VS Code and paste the following content:
# Node.js Pipeline App
[](https://dev.azure.com/YOUR_ORG/nodejs-cicd-pipeline/_build/latest?definitionId=1&branchName=main)
A simple Node.js Express application with an Azure DevOps CI/CD pipeline.
## Running Locally
```bash
npm install
npm start
```
## Running Tests
```bash
npm test
```
What does this file do?
The [] line is the badge markdown you copied from Azure DevOps. Replace the placeholder line shown above with the actual markdown you copied. Your copied version will already contain your real organization name and pipeline name.
The rest of the file gives anyone visiting your repository a quick overview of how to run the app and its tests.
- Replace the [] line in your file with the actual badge markdown you copied from Azure DevOps.
- Save README.md.
- Commit and push your README to GitHub by running:
git add .
git commit -m "Add README with status badge"
git push
Getting a permission or authentication error?
If git push fails with a credentials error, confirm you are using the same authentication method (HTTPS with token or SSH) that worked when you first pushed in Step 2.
help me fix a git push authentication error when pushing to GitHub
Verify the badge and CI flow
You have now pushed the README to GitHub. Let's confirm two things: the badge is visible and the push itself triggered another pipeline run.
- Open your nodejs-pipeline-app repository on GitHub in your browser.
- Scroll down on the repository page. You should see your README rendered with a status badge at the top showing Build Status in green (passing).
- Return to Pipelines in Azure DevOps. Confirm that another pipeline run appeared, triggered by your "Add README with status badge" commit.
Why did the badge trigger a build?
The badge itself is just a static image link. The build wasn't triggered by the badge. It was triggered because you pushed a new commit to the main branch.
Every push to main triggers the pipeline. That is your CI trigger doing its job.
Badge showing as a broken image?
Make sure you pasted the exact markdown that Azure DevOps generated for you. If you used the placeholder line from the template instead, the image URL won't resolve because YOUR_ORG and YOUR_PIPELINE_NAME are not real values.
Go back to your pipeline's three-dot menu, select Status badge, copy the markdown again, and replace the line in README.md.
help me fix my Azure DevOps pipeline status badge not showing on GitHub
✔️ Awesome, I've got everything!
Great. Double check you have saved README.md and pushed it to GitHub.
ⓧ I'd like to double check the full code
Here is what your README.md should look like (with your actual badge values replacing the placeholders):
# Node.js Pipeline App
[](https://dev.azure.com/YOUR_ORG/nodejs-cicd-pipeline/_build/latest?definitionId=1&branchName=main)
A simple Node.js Express application with an Azure DevOps CI/CD pipeline.
## Running Locally
```bash
npm install
npm start
```
## Running Tests
```bash
npm test
```
Your pipeline now runs on every push with zero manual intervention. Next up is the Secret Mission, where you will use this same automation to block broken code from being merged.
Secret mission
Add PR Validation
Your pipeline runs on every push, but what about pull requests? In real teams, broken code should never reach the main branch. Add a PR trigger and branch policy so that every pull request must pass the build and tests before it can be merged.
Clean Up Your Resources
Clean Up Your Resources
This project uses the Azure DevOps free tier, so there are no ongoing costs from leaving your pipeline in place. 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 (nodejs-cicd-pipeline).
- GitHub repository (nodejs-pipeline-app).
- Local project folder (nodejs-pipeline-app) on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building on this pipeline or plan to extend it soon.
- Your Azure DevOps pipeline will continue to run automatically on each push to GitHub.
- The free tier includes one concurrent job with up to 30 hours per month, so there is no charge.
- Your local nodejs-pipeline-app folder and GitHub repository stay as they are.
Pause - I'll come back to this later
Free up local disk space while keeping cloud resources intact for later.
- Your Azure DevOps project and GitHub repository remain untouched. They cost nothing on the free tier.
- Optionally, delete the local project folder to reclaim disk space. You can always clone the repository again later.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
Delete the Azure DevOps project
- Sign in to your organization at dev.azure.com.
- Select Organization settings in the bottom-left corner.
- Select Projects, check the box next to nodejs-cicd-pipeline, and select Delete.
- Confirm by typing the project name, then select Delete in the popup.
Delete the GitHub repository
- Go to your nodejs-pipeline-app repository on GitHub.
- Click Settings.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository.
- Read the warnings and click I have read and understand these effects.
- Type nodejs-pipeline-app to confirm, then click Delete this repository.
Delete the local project folder
- Delete the local folder by running this command:
macOS
cd ~/Desktop
rm -rf nodejs-pipeline-app
Windows
cd ~\Desktop
rmdir /s /q nodejs-pipeline-app
Nice Work!
Nice Work!
Nice work! You just built a production-style CI/CD pipeline from scratch using Azure DevOps and GitHub.
You've learned how to:
- Wrote a multi-stage YAML pipeline with Build and Package stages that installs dependencies, runs tests, and produces deployable artifacts.
- Configured CI triggers that automatically run the pipeline on every push to GitHub.
- Published pipeline artifacts and added a status badge to your repository README so your build health is always visible.
- Added PR validation with branch policies to enforce code quality before merging into main.
Ready to quiz yourself?