Build a CI/CD Pipeline with Azure DevOps

Build a multi-stage YAML pipeline that auto-deploys a Node.js app to Azure.

Introduction

30 Second Summary

Every time you use an app and it has a new feature overnight, someone did not manually copy files to a server. A CI/CD pipeline shipped that update automatically.

In this project, you will build a complete CI/CD pipeline using Azure DevOps that automatically builds and deploys a Node.js web app every time you push code.

What You'll Build

A multi-stage YAML pipeline that takes your code from a Git push to a live web application running on Azure App Service.

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

  • A working Azure DevOps project with a Git repository and a pipeline defined entirely in YAML.
  • A live Node.js web application deployed to Azure App Service at a public URL.
  • A multi-stage pipeline that automatically builds and deploys on every push to the main branch.
  • Secret Mission: Add a Test stage that blocks deployment if automated tests fail.

Are there any prerequisites?

You need a Microsoft account (personal or work/school) and an Azure subscription. Pay-as-you-go or a subscription with credits works fine.

The Azure Free Trial is NOT supported for Azure DevOps billing, but you will stay within the free tier so no billing setup is required. Basic familiarity with Git (clone, commit, push) is helpful.

Before We Start

Modern development teams need a way to ship code reliably and repeatedly. Instead of manually deploying every change, a CI/CD pipeline automates the entire process from code commit to live deployment.

In this step, you will set up your Azure DevOps organization and project, verify your Azure subscription, and install the command-line tools you need for the rest of this project.

In this step, get ready to:
  • Create an Azure DevOps organization and project.
  • Verify your Azure subscription.
  • Install Azure CLI and Git on your machine.
Create your Azure DevOps organization

Azure DevOps is where your pipeline, code repository, and project management will live. You need an organization (the top-level container) and a project inside it.

  • Open your browser and navigate to https://dev.azure.com.
  • Sign in with your Microsoft account.
  • Select New organization.
  • Enter a name for your organization, choose a hosting geography closest to you, select your Azure subscription, then click Continue.

What is an Azure DevOps organization?

An organization is the top-level container in Azure DevOps. It groups related projects, manages billing, and controls who has access. You can reach your organization anytime at https://dev.azure.com/{your-org-name}.

After the organization is created, Azure DevOps prompts you to create your first project.

  • Enter my-cicd-project as the project name.
  • Under Version control, select Git.
  • Under Work item process, select Agile.
  • Click Create.

You should see the project welcome page confirming that your project was created successfully.

Why Git and Agile?

Git is the industry-standard distributed version control system. Agile is a lightweight work-item process template that gives you boards and sprints without overhead you don't need for this project.

Don't see the New organization option?

Make sure you are signed in with a Microsoft account that has an active Azure subscription linked. Try opening an incognito or private browser window and signing in again.

Help me troubleshoot creating my Azure DevOps organization.

Verify your Azure subscription

You need an active Azure subscription to create the App Service resource in a later step. The subscription also links to your Azure DevOps organization for billing (though you will stay within the free tier).

  • Open a new browser tab and navigate to https://portal.azure.com.
  • Sign in with the same Microsoft account you used for Azure DevOps.
  • In the search bar at the top, type Subscriptions and select the Subscriptions service.
  • Confirm that you see at least one active subscription listed (for example, Pay-As-You-Go or a subscription with credits).

Using Azure Free Trial? Read this first

Azure DevOps does not support the Azure Free Trial for billing. However, you do not need to set up billing because this project stays within the free tier (1,800 pipeline minutes per month).

You do need a regular Azure subscription (such as Pay-As-You-Go) to create the Azure App Service resource later. The App Service Free F1 plan costs $0.

Install Azure CLI and Git

The Azure CLI lets you create and manage Azure resources from the command line. Git is the version control tool you will use to push code to your Azure Repos repository.

  • Open a terminal on your machine. 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 whether the Azure CLI is already installed by running this command:

az --version

✔️ I see a version number

The Azure CLI is installed and ready to go. Move on to the Git check below.

ⓧ Command not found

You need to install the Azure CLI. Follow the instructions for your operating system.

macOS

  • Install the Azure CLI with Homebrew by running:
brew install azure-cli

If you do not have Homebrew installed, visit https://brew.sh and follow the install instructions first.

Windows

  • Install the Azure CLI with WinGet by running:
winget install --exact --id Microsoft.AzureCLI

After installation finishes, close and reopen your terminal for the az command to become available.

After installing, verify by running az --version again. You should see a version number in the output.

Next, check whether Git is installed by running:

git --version

✔️ I see a version number

Git is installed and ready. You are all set for the next step.

ⓧ Command not found

You need to install Git. Follow the instructions for your operating system.

macOS

  • Install Git using Homebrew by running:
brew install git

Alternatively, running any git command on macOS will prompt you to install the Xcode Command Line Tools, which include Git.

Windows

  • Download and install Git from the official site at https://git-scm.com/download/win.
  • Run the installer and accept the default settings.
  • After installation, close and reopen your terminal.

After installing, run git --version again. You should see a version number in the output.

Still seeing command not found?

Make sure you closed and reopened your terminal after installation. New installations only appear in terminals opened after the install completes.

Help me troubleshoot installing Azure CLI or Git.

Your accounts and tools are ready. Next up, you will create the Node.js application that your pipeline will build and deploy.

Set Up Your Azure Accounts and Tools

Before you can build a pipeline, you need the accounts and tools that power it. Azure DevOps hosts your code and runs your pipeline, while an Azure subscription gives you a deployment target.

In this step, you will create your Azure DevOps organization and project, confirm your Azure subscription is active, and install the two command-line tools the project depends on.

In this step, get ready to:
  • Create an Azure DevOps organization and project.
  • Verify your Azure subscription.
  • Install and verify Azure CLI and Git.
Create your Azure DevOps organization

An Azure DevOps organization is the top-level container that holds all your projects, repos, and pipelines. You need one before you can store code or run builds.

  • Open your browser and navigate to dev.azure.com.
  • Click Start free and sign in with your Microsoft account.
  • If prompted, review the Terms of Service and select Continue.

What is an Azure DevOps organization?

An organization groups related projects under a single billing and security boundary. You access yours at a URL like https://dev.azure.com/YourOrgName.

  • If you do not already have an organization, click Create new organization.
  • Enter a name for your organization and choose a hosting region close to you.
  • Click Continue.

After your organization is created, Azure DevOps prompts you to create your first project.

  • Enter my-cicd-project as the project name.
  • Under Version control, select Git.
  • Under Work item process, select Agile.
  • Click Create.

You should see the project welcome page confirming your new project is ready.

Don't see the Create button?

Make sure you are signed in with the same Microsoft account you used to create the organization. If the page does not load, try clearing your browser cache or using an incognito window.

help me troubleshoot creating my Azure DevOps project

Verify your Azure subscription

Your pipeline will deploy to Azure App Service, which lives inside an Azure subscription. You need an active subscription to create cloud resources later in the project.

  • Open a new browser tab and navigate to portal.azure.com.
  • Sign in with your Microsoft account.
  • In the search bar at the top, type Subscriptions and select Subscriptions from the results.
  • Confirm you see at least one subscription with a status of Active.

Don't have an active subscription?

You need a Pay-as-you-go subscription or a subscription with available credits. The Azure Free Trial is NOT supported for Azure DevOps billing.

However, you do not need to set up billing for Azure DevOps itself since this project stays within the free tier (1,800 minutes per month). You only need the subscription to create the App Service resource in a later step.

Install Azure CLI and Git

The Azure CLI lets you create and manage Azure resources from your terminal. Git is the version control tool you will use to push code to Azure Repos.

Let's check whether both tools are already installed on your machine.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal and pressing Enter.
  • Check whether Azure CLI is installed by running:
az --version

✔️ I see a version number

Azure CLI is installed and ready to go.

ⓧ Command not found

You need to install the Azure CLI. Follow the instructions for your operating system.

macOS

  • Install Azure CLI using Homebrew by running:
brew install azure-cli

If Homebrew prompts you for confirmation, enter y and press Enter.

  • After installation completes, close and reopen your terminal, then run az --version again to confirm it works.

Windows

  • Install Azure CLI using WinGet by running this in PowerShell:
winget install --exact --id Microsoft.AzureCLI

When the installer asks if it can make changes to your computer, select Yes.

  • After installation completes, close and reopen your terminal, then run az --version again to confirm it works.
  • Check whether Git is installed by running:
git --version

✔️ I see a version number

Git is installed and ready to go.

ⓧ Command not found

You need to install Git.

  • Download and install Git from git-scm.com/downloads.
  • Follow the installer prompts, accepting the defaults.
  • After installation completes, close and reopen your terminal, then run git --version again to confirm it works.

Still seeing command not found?

Make sure you closed and reopened your terminal after installing. New installations only become available in a fresh terminal session.

help me troubleshoot Azure CLI or Git installation

Your accounts and tools are all set. Next up, you will create your Node.js application files and push them into Azure Repos.

Create Your Node.js App in Azure Repos

Your pipeline needs something to build and deploy. In this step, you'll create the web application that will travel through your CI/CD pipeline on every push.

The app is a minimal Node.js server built with Express. It's intentionally simple so you can focus on the pipeline mechanics rather than application complexity.

In this step, get ready to:
  • Clone your Azure Repos repository locally.
  • Create three application files: package.json, index.js, and test.js.
  • Push the code to your Azure Repos main branch.
Clone your Azure Repos repository

Before you can add files, you need a local copy of your Azure Repos Git repository on your machine.

  • In your Azure DevOps project, click Repos in the left sidebar.
  • Click Clone in the upper-right corner of the Files page.
  • Copy the HTTPS clone URL.

What is this URL?

The clone URL is the address of your remote repository hosted in Azure DevOps. Git uses it to download a full copy to your machine so you can work on files locally.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the repository by running this command:
git clone [[REPOURL="your Azure Repos HTTPS clone URL"]]

What happens when you run this?

Git will prompt you to authenticate. You may see a browser window, a credential manager pop-up, or a terminal prompt asking for your credentials. Sign in with the same Microsoft account you used for Azure DevOps.

Once authenticated, Git downloads the repository into a new folder on your Desktop named after your project (e.g. my-cicd-project).

  • Move into the cloned folder by running this command:
cd [[REPOFOLDER="your cloned repository folder name"]]
  • Confirm you're inside the repository by running:
git status

You should see a message indicating you are on the main branch with nothing to commit.

Authentication failed?

If the clone fails with an authentication error, confirm you're using the HTTPS URL (not SSH). Try signing out and back in to your Microsoft account, then re-run the clone command.

help me troubleshoot my Azure Repos clone authentication error

Create the Node.js application files

You'll create three files in the root of your repository. Each one has a specific job: one defines the project metadata, one runs the web server, and one tests that the server works.

  • Create a new file called package.json in your repository folder and paste the following code:
{
  "name": "azure-devops-cicd-app",
  "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": "node test.js"
  },
  "dependencies": {
    "express": "4.21.2"
  }
}

What does this file do?

The package.json file is the manifest for a Node.js project. It declares the project name, entry point, and dependencies.

The scripts section defines two commands: start launches the server, and test runs the test file. Your pipeline will use both of these.

  • Save package.json.
  • Create a new file called index.js in the same folder and paste the following code:
// Import the Express framework
const express = require('express');
const app = express();

// Use the PORT environment variable (set by Azure App Service) or default to 3000
const port = process.env.PORT || 3000;

// Define the root route
app.get('/', (req, res) => {
  res.send('Hello from Azure DevOps CI/CD!');
});

// Start the server
app.listen(port, () => {
  console.log(`App running on port ${port}`);
});

// Export the app so the test file can use it
module.exports = app;

What does this file do?

This is your web application. It creates an Express server that responds with a greeting when someone visits the root URL.

The process.env.PORT line is important. Azure App Service provides a PORT environment variable that tells your app which port to listen on. Locally, it falls back to port 3000.

The module.exports = app line at the bottom lets the test file import and start the server without duplicating code.

  • Save index.js.
  • Create a new file called test.js in the same folder and paste the following code:
// Import the app from index.js
const app = require('./index');

// Start the server on a random available port
const server = app.listen(0, () => {
  const port = server.address().port;
  const http = require('http');

  // Make a request to the running server
  http.get(`http://localhost:${port}/`, (res) => {
    let data = '';
    res.on('data', (chunk) => { data += chunk; });
    res.on('end', () => {
      // Check if the response matches what we expect
      if (data === 'Hello from Azure DevOps CI/CD!') {
        console.log('TEST PASSED: Response matches expected output');
        server.close();
        process.exit(0);
      } else {
        console.error(`TEST FAILED: Expected "Hello from Azure DevOps CI/CD!" but got "${data}"`);
        server.close();
        process.exit(1);
      }
    });
  }).on('error', (err) => {
    console.error('TEST FAILED:', err.message);
    server.close();
    process.exit(1);
  });
});

What does this file do?

This is your automated test. It starts the Express server on a random port, makes an HTTP request to it, and checks that the response matches the expected greeting.

If the response is correct, it prints TEST PASSED and exits with code 0 (success). If anything goes wrong, it prints TEST FAILED and exits with code 1 (failure). Your pipeline will use this exit code to decide whether the build is healthy.

  • Save test.js.
  • Confirm all three files exist by running:
ls

You should see index.js, package.json, and test.js listed in the output.

Missing a file?

Make sure you saved each file in the root of your cloned repository folder, not inside a subfolder. Run pwd to confirm you're in the right directory.

help me find where my files were saved

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files.

ⓧ I'd like to double check the full code

Here are the complete contents of all three files for comparison.

{
  "name": "azure-devops-cicd-app",
  "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": "node test.js"
  },
  "dependencies": {
    "express": "4.21.2"
  }
}
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

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

app.listen(port, () => {
  console.log(`App running on port ${port}`);
});

module.exports = app;
const app = require('./index');

const server = app.listen(0, () => {
  const port = server.address().port;
  const http = require('http');

  http.get(`http://localhost:${port}/`, (res) => {
    let data = '';
    res.on('data', (chunk) => { data += chunk; });
    res.on('end', () => {
      if (data === 'Hello from Azure DevOps CI/CD!') {
        console.log('TEST PASSED: Response matches expected output');
        server.close();
        process.exit(0);
      } else {
        console.error(`TEST FAILED: Expected "Hello from Azure DevOps CI/CD!" but got "${data}"`);
        server.close();
        process.exit(1);
      }
    });
  }).on('error', (err) => {
    console.error('TEST FAILED:', err.message);
    server.close();
    process.exit(1);
  });
});
Push your code to Azure Repos

Your files exist locally but your Azure DevOps pipeline can only access code that lives in the remote repository. You need to commit and push.

  • Stage all files for commit by running:
git add .
  • Commit the files with a message by running:
git commit -m "Initial app"
  • Push the commit to the remote main branch by running:
git push origin main

What do these commands do?

The git add . command stages all new and modified files. The git commit command saves a snapshot of those changes locally.

The git push origin main command uploads your local commits to the main branch on Azure Repos. Once pushed, your code is visible in the Azure DevOps web interface.

  • In your browser, return to your Azure DevOps project.
  • Click Repos in the left sidebar, then click Files.

You should see index.js, package.json, and test.js listed in the repository.

Files not showing in Azure Repos?

Run git log --oneline to confirm your commit exists locally. If it does, the push may have failed silently. Re-run git push origin main and check for authentication errors.

help me figure out why my push to Azure Repos is not working

Your application code is live in Azure Repos. Next up, you'll create the Azure App Service that will host your deployed application.

Create the Azure App Service

Your code is ready in Azure Repos. Now you need somewhere for that code to actually run.

In this step, you will create an Azure App Service using the Azure CLI. This gives your pipeline a deployment target where it can push your app live to the internet.

In this step, get ready to:
  • Sign in to your Azure subscription from the terminal.
  • Create a resource group, App Service plan, and web app using the Azure CLI.
  • Verify the App Service is running in your browser.
Log in to Azure CLI

Before you can create any resources in Azure, you need to authenticate. The az login command opens a browser window where you sign in with your Microsoft account.

  • Sign in to Azure by running this command in your terminal:
az login

What does this command do?

This opens your browser and asks you to sign in with the Microsoft account linked to your Azure subscription. Once you authenticate, the CLI stores a session token so all future commands run against your subscription.

  • A browser window opens asking you to sign in. Use the Microsoft account linked to your Azure subscription.
  • After signing in, your terminal displays a list of subscriptions in JSON format. Confirm you see at least one subscription with a state of Enabled.

Not seeing any subscriptions?

Make sure you are signing in with the same Microsoft account that has an active Azure subscription (Pay-as-you-go or credit-based). The Azure Free Trial does NOT work for Azure DevOps billing, but it is sufficient for creating the App Service resource in this step.

help me troubleshoot why az login is not showing my Azure subscription

Create the App Service resources

You need three resources to host your app: a resource group (a container for related resources), an App Service plan (the compute tier), and a web app (the actual hosting slot). You will create all three using the Azure CLI.

  • Create a resource group by running this command:
az group create --name my-cicd-rg --location eastus

What does this command do?

A resource group is a logical container that holds related Azure resources. The --name flag sets the group name, and --location sets the Azure region. You can pick a different region if one is closer to you (e.g. westus, westeurope).

  • Confirm you see a JSON response containing provisioningState set to Succeeded in the output.
  • Create an App Service plan on the Free F1 tier by running:
az appservice plan create --name my-cicd-plan --resource-group my-cicd-rg --sku F1 --is-linux

What does this command do?

An App Service plan defines the compute resources (CPU, memory) for your web apps. The --sku F1 flag selects the Free tier, which costs $0. The --is-linux flag creates a Linux-based plan, which is required for running Node.js natively.

  • Confirm the output shows a JSON block with your plan name and a status of Ready.
  • Create the web app with the Node.js 20 LTS runtime by running:
az webapp create --resource-group my-cicd-rg --name [[APPNAME="your-unique-app-name"]] --plan my-cicd-plan --runtime 'NODE:20-lts'

What does this command do?

This creates the actual web app that will serve your Node.js code. The --name must be globally unique across all of Azure because it becomes part of your public URL. Valid characters are lowercase letters, numbers, and hyphens.

A good naming pattern is your name plus a project identifier, like jane-cicd-app. The --runtime flag tells Azure to use the Node.js 20 LTS runtime for your app.

  • Confirm the output shows a JSON response containing the defaultHostName field with your app URL.

Seeing a naming conflict error?

If you see an error about the name already being taken, choose a more unique name. Try adding random numbers or your initials to make it distinct.

help me fix a naming conflict when running az webapp create

Verify the App Service exists

Your App Service is now live. Before you wire up the pipeline, confirm it is accessible in your browser.

  • Open your browser and navigate to your App Service URL:
https://[[APPNAME="your-unique-app-name"]].azurewebsites.net
  • You should see a default placeholder page confirming the App Service is running.

Why a placeholder page?

You have not deployed your code yet. Azure shows a default landing page to prove the infrastructure is working. Once your pipeline deploys the app, this page will be replaced with your Express app response.

Seeing a 404 or error page?

Wait 1-2 minutes after creating the web app and refresh the page. New App Service instances sometimes take a moment to start. If it still does not load, double check that you used the correct app name in the URL.

help me troubleshoot why my Azure App Service URL shows an error

Your deployment target is live. Next up, you will connect Azure DevOps to your Azure subscription so the pipeline has permission to deploy code to this App Service.

Connect Azure DevOps to Your Azure Subscription

Your App Service is live and waiting for code, but Azure DevOps has no idea it exists. These are two separate services that cannot talk to each other by default.

To fix this, you will create a service connection in Azure DevOps. This is a secure link that gives your pipeline permission to deploy directly to your Azure resources.

In this step, get ready to:
  • Create an Azure Resource Manager service connection.
  • Configure the connection's security settings.
  • Understand how pipeline authorization works.
Create the service connection

A service connection authenticates Azure DevOps to your Azure subscription so the pipeline can deploy code without you manually logging in each time.

  • Return to your Azure DevOps project in the browser.
  • Click the Project settings gear icon in the bottom-left corner of the sidebar.
  • Under the Pipelines section in the left menu, click Service connections.
  • Click New service connection (or Create service connection if this is your first one).
  • Select Azure Resource Manager from the list.
  • Click Next.
  • For Identity Type, select App registration (automatic) from the dropdown menu.
  • For Credential, leave the default value: Workload identity federation.
  • For Scope level, select Subscription.
  • Select your Azure subscription from the dropdown.
  • Optionally, select your resource group (my-cicd-rg) to limit access to only that resource group.

What is workload identity federation?

Workload identity federation is a modern authentication method that eliminates the need for stored secrets or passwords. Azure DevOps proves its identity to Azure using a short-lived token, which Azure verifies without any shared credential.

This is more secure than older approaches that required you to manually manage client secrets with expiration dates.

Name and save the connection
  • In the Service connection name field, enter azure-subscription-connection.
  • Make sure the Grant access permission to all pipelines checkbox is not checked.
  • Click Save.

Why leave the all-pipelines checkbox unchecked?

Granting access to all pipelines means any pipeline in the project can deploy to your Azure subscription without approval. This is a security risk in real teams where multiple pipelines exist.

Instead, you will authorize each pipeline individually on its first run. This gives you control over exactly which pipelines can touch your production resources.

After saving, you should see your new azure-subscription-connection listed on the Service connections page.

Don't see your subscription in the dropdown?

This usually means your Microsoft account does not have the required permissions on the Azure subscription. Confirm that you are signed into Azure DevOps with the same Microsoft account that owns your Azure subscription.

If you are using a work or school account, your directory administrator may need to grant you permission to create app registrations.

Help me troubleshoot why my Azure subscription isn't showing up in the service connection dropdown.

Understand pipeline authorization

Because you left the all-pipelines checkbox unchecked, your pipeline will need explicit approval to use this connection the first time it runs.

  • When you run the pipeline for the first time (in the next step), Azure DevOps will show a permission prompt asking you to authorize the pipeline to use this service connection.
  • At that point, click Permit to grant your specific pipeline access.

One-time approval only

You only need to approve the pipeline once. After the first authorization, all future runs of that same pipeline can use the service connection without additional prompts.

Your Azure DevOps project can now reach your Azure subscription securely. Next up, you will write the YAML pipeline that uses this connection to build and deploy your app automatically.

Write Your CI/CD Pipeline in YAML

Your Azure DevOps project can now talk to your Azure subscription through the service connection you created. The next step is to tell Azure DevOps exactly what to do with that connection.

A YAML pipeline file defines every stage of your CI/CD process as code, checked into your repository alongside your app. Azure DevOps reads this file on every push and executes the stages in order.

In this step, get ready to:
  • Create the azure-pipelines.yml file in your repository.
  • Write a Build stage that packages your app into a deployable ZIP.
  • Write a Deploy stage that pushes the ZIP to Azure App Service.
Create the pipeline file

Every Azure DevOps YAML pipeline lives in a file at the root of your repository. When you push this file, Azure DevOps detects it and offers to run the pipeline automatically.

  • Create the pipeline file in your repository by running this command from your cloned project folder:
touch azure-pipelines.yml
  • Confirm the file exists by running:
ls

You should see azure-pipelines.yml listed alongside package.json, index.js, and test.js.

Write the Build stage

The Build stage is where your pipeline installs dependencies, packages the application into a ZIP file, and publishes it as a pipeline artifact. A Microsoft-hosted agent running Ubuntu handles the work for you in the cloud.

  • Open azure-pipelines.yml in your code editor and paste the following code:
# Trigger the pipeline on pushes to the main branch
trigger:
  - main

# Define reusable variables for the pipeline
variables:
  azureSubscription: 'azure-subscription-connection'
  webAppName: '[[WEBAPP_NAME="your-app-name-here"]]'
  vmImageName: 'ubuntu-latest'

# Define the stages of the pipeline
stages:
- stage: Build
  displayName: 'Build Stage'
  jobs:
  - job: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    # Install Node.js on the build agent
    - task: NodeTool@0
      inputs:
        versionSpec: '20.x'
      displayName: 'Install Node.js'

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

    # Package the app into a ZIP for deployment
    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
        replaceExistingArchive: true
      displayName: 'Archive files'

    # Publish the ZIP as a pipeline artifact named 'drop'
    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop
      displayName: 'Publish artifact'

What does this code do?

The trigger section tells Azure DevOps to run this pipeline every time someone pushes to the main branch. The variables section defines reusable values so you only set your service connection name and app name once.

Inside the Build stage, four steps execute in sequence. NodeTool@0 installs Node.js 20.x on the agent. npm install fetches your app's dependencies. ArchiveFiles@2 compresses everything into a ZIP file. Finally, the publish step uploads that ZIP as an artifact named drop so the Deploy stage can pick it up.

  • Replace your-app-name-here in the webAppName variable with the actual name of the Azure App Service you created in the previous step.

Why YAML over a Classic pipeline?

YAML pipelines store your CI/CD configuration as code, versioned in the same repository as your application. If something breaks, you can trace exactly which pipeline change caused the issue using Git history.

Classic pipelines use a GUI editor that lives only inside Azure DevOps. Changes are harder to review, share, and roll back. The industry has moved toward pipeline-as-code for this reason.

Seeing YAML indentation errors?

YAML is whitespace-sensitive. Make sure each level of nesting uses exactly two spaces (not tabs). The stages: key sits at the root level, each - stage: is indented two spaces under it, and so on.

Help me fix YAML indentation errors

Write the Deploy stage

The Deploy stage picks up the ZIP artifact from the Build stage and pushes it to your Azure App Service. It only runs if the Build stage succeeds.

  • In azure-pipelines.yml, add the following code directly below the Build stage (after the last displayName: 'Publish artifact' line):
# Deploy stage - pushes the artifact to Azure App Service
- stage: Deploy
  displayName: 'Deploy Stage'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    pool:
      vmImage: $(vmImageName)
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          # Deploy the ZIP package to Azure App Service
          - task: AzureWebApp@1
            inputs:
              azureSubscription: $(azureSubscription)
              appType: 'webAppLinux'
              appName: $(webAppName)
              package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
            displayName: 'Deploy to Azure App Service'

What does this code do?

The dependsOn: Build line ensures Deploy waits for Build to finish. The condition: succeeded() line means Deploy only runs if Build passed. If your build fails, the pipeline stops and nothing deploys.

The AzureWebApp@1 task uses your service connection (azureSubscription) to authenticate with Azure, then pushes the ZIP artifact to your App Service. The deployment job type with strategy: runOnce is Azure DevOps's pattern for deployment jobs that target an environment.

  • Save azure-pipelines.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved azure-pipelines.yml and that both the Build stage and Deploy stage are present.

ⓧ I'd like to double check the full code

Here is the complete azure-pipelines.yml file. Compare your file against it:

trigger:
  - main

variables:
  azureSubscription: 'azure-subscription-connection'
  webAppName: '[[WEBAPP_NAME="your-app-name-here"]]'
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: 'Build Stage'
  jobs:
  - job: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: NodeTool@0
      inputs:
        versionSpec: '20.x'
      displayName: 'Install Node.js'

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

    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
        replaceExistingArchive: true
      displayName: 'Archive files'

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop
      displayName: 'Publish artifact'

- stage: Deploy
  displayName: 'Deploy Stage'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    pool:
      vmImage: $(vmImageName)
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            inputs:
              azureSubscription: $(azureSubscription)
              appType: 'webAppLinux'
              appName: $(webAppName)
              package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
            displayName: 'Deploy to Azure App Service'
  • Commit and push azure-pipelines.yml to Azure Repos by running these commands:
git add azure-pipelines.yml
git commit -m "Add CI/CD pipeline"
git push origin main

What does this do?

These commands stage your new pipeline file, commit it with a descriptive message, and push it to the main branch in Azure Repos. Once it arrives, Azure DevOps will detect the file and make the pipeline available to run.

Push rejected or authentication error?

If Git prompts for credentials again, use the same credentials you set up when you originally cloned the repo. If you see a rejection, make sure no one else has pushed to main in the meantime. Try pulling first with git pull origin main then pushing again.

Help me troubleshoot push issues

Your pipeline is defined and pushed. Next up, you will watch it run in Azure DevOps and see your app deploy live.

Run the Pipeline and See Your App Live

Your pipeline definition is written, committed, and sitting in the repository. Now it is time to see the magic of CI/CD in action.

In this step, you will create the pipeline in Azure DevOps, authorize the service connection, and watch your code travel from repository to live web app without touching a server.

In this step, get ready to:
  • Trigger the pipeline in Azure DevOps.
  • Watch the Build and Deploy stages execute.
  • Verify your app is live at its public URL.
Create and trigger the pipeline

Azure DevOps does not automatically run a YAML pipeline just because the file exists in the repo. You need to tell Azure DevOps to create a pipeline that points to your azure-pipelines.yml file.

  • In your Azure DevOps project, click Pipelines in the left sidebar.
  • Click New pipeline.
  • On the Connect tab, select Azure Repos Git as your source code location.
  • Select your repository (the one containing your app code and azure-pipelines.yml).
  • On the Configure step, select Existing Azure Pipelines YAML file.
  • Choose the main branch and select /azure-pipelines.yml as the path.
  • Click Continue.
  • Review the YAML file that appears. It should match the pipeline you wrote in the previous step.
  • Click Run to save the pipeline and trigger the first run.

Why do I need to create a pipeline manually?

Having an azure-pipelines.yml file in your repo is just the definition. Azure DevOps still needs you to register the pipeline so it knows which file to use and which triggers to listen for.

After this one-time setup, every future push to main triggers the pipeline automatically.

Azure DevOps will immediately queue a run because your YAML file has a trigger on the main branch and code already exists there.

What is the permission prompt?

On the first run, Azure DevOps checks whether the pipeline is allowed to use the service connection you created earlier. You will see a banner that says the pipeline needs permission to access a resource.

Click Permit to authorize the pipeline to use the azure-subscription-connection service connection. This is a one-time approval per pipeline.

  • If you see a Permission needed banner at the top of the pipeline run, click Permit to allow the pipeline to use the service connection.
  • In the confirmation dialog, click Permit again.

Don't see the pipeline after clicking Run?

Make sure you selected Azure Repos Git (not GitHub) on the Connect tab. Also confirm you picked the correct repository where your azure-pipelines.yml lives.

help me figure out why my Azure DevOps pipeline is not appearing after I clicked Run

Watch the pipeline run

Once you have authorized the service connection, the pipeline starts executing. This is where you see your YAML come to life as a sequence of automated tasks.

  • Click on the running pipeline to open the detailed view.
  • You will see two stages displayed in sequence: Build Stage and Deploy Stage.
  • Click on Build Stage to expand its logs.
  • Watch the tasks execute: Install Node.js, Install dependencies, Archive files, and Publish artifact.
  • Once the Build Stage completes with a green checkmark, the Deploy Stage begins automatically.
  • Click on Deploy Stage to see the Deploy to Azure App Service task push your packaged app to the cloud.

What is happening in each stage?

The Build Stage uses a Microsoft-hosted agent running ubuntu-latest to install Node.js, run npm install, compress everything into a ZIP file, and publish that ZIP as a pipeline artifact.

The Deploy Stage downloads that artifact and uses the AzureWebApp@1 task to push the ZIP to your Azure App Service. The service connection you authorized earlier gives the pipeline permission to deploy on your behalf.

The full pipeline run typically takes 2 to 4 minutes. Wait until both stages show a green checkmark.

Seeing a failed Deploy Stage?

Check that the webAppName variable in your azure-pipelines.yml matches the exact name of the App Service you created in Step 3. Also verify your service connection name azure-subscription-connection matches what you named it in Project settings.

help me debug a failed Deploy stage in my Azure DevOps pipeline deploying to Azure App Service

Verify your app is live

Once both stages complete successfully, your Node.js app is running on Azure. Time to confirm it works.

  • Open a new browser tab.
  • Navigate to your App Service URL:
https://[[APPNAME="your-app-name"]].azurewebsites.net
  • You should see the text Hello from Azure DevOps CI/CD! displayed in your browser.

What does this confirm?

Seeing this message proves the full CI/CD loop works end to end. Your code went from an Azure Repos commit to a packaged artifact to a live web app, all without manually copying files or SSHing into a server.

Still seeing the default Azure placeholder page?

The deployment might still be propagating. Wait 30 seconds and refresh the page. If the placeholder persists, return to the pipeline run and confirm the Deploy Stage shows a green checkmark (not a skip or failure).

help me figure out why my Azure App Service still shows the default page after a successful pipeline deployment

Secret mission

Add a Test Stage

Your pipeline builds and deploys, but what if broken code slips through? Add a Test stage that runs automated tests and blocks deployment when they fail. This is how production teams prevent bugs from reaching users.

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 App Service Free F1 tier and Azure DevOps free tier, so there are no ongoing costs.

Resources you used:

  • Azure resource group my-cicd-rg (contains your App Service plan and web app).
  • Azure DevOps project (contains your repository, pipeline, and service connection).
  • Local project folder (the cloned repository on your machine).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with the pipeline.

  • The App Service Free F1 tier costs $0/month. Your app stays live at its .azurewebsites.net URL.
  • Azure DevOps gives you 1,800 free pipeline minutes per month. Your pipeline triggers on every push to main.
  • Your local project folder stays on your machine for future edits.

Pause - I'll come back to this later

Stop the App Service so it does not respond to requests while idle. No charges accrue on the Free tier either way, but stopping it makes clear the project is paused.

  • Stop your App Service by running this command in your terminal:
az webapp stop --name [[APPNAME="your-app-name"]] --resource-group my-cicd-rg
  • When you are ready to resume, start it again by running:
az webapp start --name [[APPNAME="your-app-name"]] --resource-group my-cicd-rg
  • Your pipeline, repository, and service connection remain intact in Azure DevOps.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is permanent.

  • Delete all Azure resources by removing the resource group. Run this in your terminal:
az group delete --name my-cicd-rg --yes --no-wait

This deletes the App Service plan, web app, and everything inside the my-cicd-rg resource group.

  • Delete the Azure DevOps project. In your browser, go to your Azure DevOps organization.
  • Click Organization settings in the bottom-left corner.
  • Select Projects.
  • Check the box next to your project, click Delete, then confirm by typing the project name.
  • Delete your local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your-project-folder-name"]]
  • On Windows, run:
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your-project-folder-name"]]
  • Sign out of the Azure CLI by running:
az logout

Nice Work!

Nice Work!

Nice work! You just built a fully automated CI/CD pipeline from scratch using Azure DevOps.

Every time you push code to your repository, your pipeline builds, tests, and deploys it live. That is the core loop of modern software delivery.

Here's what you accomplished:

  • Wrote a multi-stage YAML pipeline with Build and Deploy stages that automatically packages and ships your Node.js app to Azure App Service on every push.
  • Created a secure service connection using workload identity federation to link Azure DevOps to your Azure subscription without managing secrets.
  • Experienced the full CI/CD loop: push code, automatic build, automatic deploy, live app at <your-app-name>.azurewebsites.net.
  • Added a Test stage to gate deployments on passing tests, proving that broken code never reaches production.

Ready to quiz yourself?