Build a CI/CD Pipeline with Azure DevOps
Automate code deployment to Azure App Service with a YAML pipeline.
Introduction
30 Second Summary
Every time you manually upload code to a server, you risk forgetting a step, overwriting something important, or deploying a broken version. What if your code could test and deploy itself every time you made a change?
In this project, you will build a CI/CD pipeline using Azure DevOps that automatically builds, tests, and deploys a Node.js web application to the cloud every time you push code.
What You'll Build
A complete multi-stage YAML pipeline in Azure DevOps that takes a Node.js Express application from code commit to live deployment on Azure App Service.
By the end of this project, you'll have:
- A working multi-stage YAML pipeline that automatically builds and deploys your app on every push to main.
- A live web application running on Azure App Service, deployed entirely through your pipeline.
- A clear understanding of the Azure Pipelines hierarchy (Pipeline > Stage > Job > Step) and how to extend it.
- Secret Mission: Add a dedicated Test stage with quality gates that block deployment from non-main branches.
Are there any prerequisites?
You should have a basic understanding of Git (clone, commit, push). You also need a Microsoft account and an Azure subscription (a free account with $200 in credits works perfectly).
Set Up Azure DevOps and Azure Resources
Before your pipeline can build and deploy code, it needs two things: a place to live and somewhere to deploy to.
In this step, you will create an Azure DevOps organization and project to host your pipeline and code. You will also create an Azure App Service to serve as your deployment target.
In this step, get ready to:
- Create an Azure DevOps organization and project.
- Create an Azure App Service with the Free F1 plan.
- Request the free parallel job grant (if needed).
Create an Azure DevOps organization and project
An Azure DevOps organization is the top-level container that groups your projects together. Inside the organization, each project isolates its own code, pipelines, and work items.
- Open your browser and navigate to dev.azure.com.
- Sign in with your Microsoft account.
- Click New organization.
- Enter a name for your organization (e.g., your name or initials followed by a number).
- Select a hosting region close to you.
- Select your Azure subscription for billing.
- Click Continue.
What is an Azure DevOps organization?
An organization groups related projects and pipelines under one account. You access it at https://dev.azure.com/{YourOrganization}. As the creator, you automatically become the organization owner.
Azure DevOps will prompt you to create a project immediately after your organization is set up.
- Enter my-cicd-app as the project name.
- Set the Visibility to Private.
- Under version control, select Git.
- Leave the work item process as the default.
- Click Create project.
You should now see the my-cicd-app project dashboard with sections for Repos, Pipelines, and Boards.
Can't create the organization?
New Azure DevOps organizations require an active Azure subscription linked to your account. Make sure your subscription is in good standing. If you see no response when clicking Continue, try clearing your browser cache or using an InPrivate/Incognito window.
help me troubleshoot why I can't create an Azure DevOps organization
Create an Azure App Service
Azure App Service is a managed hosting platform that runs your web app without requiring you to manage servers. The Free F1 plan gives you a no-cost deployment target that is perfect for learning.
- Open a new browser tab and navigate to portal.azure.com.
- Sign in with the same Microsoft account you used for Azure DevOps.
- In the search bar at the top, type App Services and select it from the results.
- Click + Create to start creating a new Web App.
Fill in the creation form with the following settings:
- Under Resource Group, click Create new and enter a name (e.g., my-cicd-app-rg).
- In the Name field, enter a globally unique name for your app, for example my-cicd-app-yourname. This name becomes part of your app's URL.
- Set Publish to Code.
- Set Runtime stack to Node 20 LTS.
- Set Operating System to Linux.
- Select a Region close to you.
- Under Pricing plan, click Create new and select Free F1 from the pricing plan list.
- Click Review + create at the bottom of the page.
- After validation passes, click Create.
- Wait for the deployment to complete, then click Go to resource.
You should see your App Service overview page showing the default domain (e.g., https://my-cicd-app-yourname.azurewebsites.net) and a status of Running.
Why the Free F1 plan?
The Free F1 plan provides 60 CPU minutes per day, 1 GB of RAM, and 1 GB of storage at no cost. It is more than enough for a learning project and ensures you will not be charged for this deployment target.
Don't see Node 20 LTS in the runtime list?
Make sure you selected Linux as the operating system first. The available runtime stacks change depending on which OS you choose. If you still do not see it, try selecting a different region.
help me find the Node 20 LTS runtime option when creating an Azure App Service
Request the free parallel job grant (if needed)
New Azure DevOps organizations do not always have Microsoft-hosted parallel jobs enabled by default. Without at least one parallel job, your pipeline cannot run. You need to check your current allocation and request the free grant if it shows zero.
- Switch back to your Azure DevOps browser tab.
- Click the gear icon in the bottom-left corner to open Organization Settings.
- In the left sidebar, expand Pipelines and select Parallel jobs.
- Look at the Microsoft-hosted section. If it shows 0 parallel jobs, you need to request the free grant.
✔️ I see 1 or more parallel jobs
You are all set. Your organization already has Microsoft-hosted parallel jobs enabled. You can skip ahead to the next step.
ⓧ I see 0 parallel jobs
You need to request the free grant. This is a one-time process.
- Click the link in the Azure DevOps UI to request the free grant, or fill out the request form linked on the parallel jobs page.
- Provide your organization name and a brief description of your use case (e.g., "learning CI/CD").
- Submit the form.
How long does the grant take?
Microsoft typically processes free grant requests within 1-2 business days, but it can sometimes take longer. You can continue with the remaining setup steps in this project while you wait. The grant is only needed when you run the pipeline in a later step.
What is a parallel job?
A parallel job is a slot for running one pipeline at a time on a Microsoft-hosted agent. The free tier for private projects includes one concurrent job with up to 30 hours of build time per month. That is more than enough for this project.
Your Azure DevOps project and Azure App Service are ready. Next up, you will push your application code to Azure Repos and give the pipeline something to build.
Push Your Application Code to Azure Repos
Your Azure DevOps project and App Service are ready. Now your pipeline needs actual application code to build and deploy.
In this step, you'll clone the empty repository, create a Node.js Express application, and push it to Azure Repos. This gives the pipeline something to work with in the next step.
In this step, get ready to:
- Clone the Azure Repos repository to your local machine.
- Create three application files: package.json, index.js, and index.test.js.
- Push all files to the main branch in Azure Repos.
Clone the Azure Repos repository
Before you can add code, you need a local copy of the repository on your machine. You'll grab the clone URL from Azure DevOps and use Git to download it.
- Switch back to your Azure DevOps project at dev.azure.com.
- Click Repos in the left sidebar.
- Click the Clone button in the upper-right area of the page.
- Copy the HTTPS clone URL. It looks like https://dev.azure.com/{your-org}/my-cicd-app/_git/my-cicd-app.
What is cloning?
Cloning creates a complete local copy of a remote repository on your computer. It also sets up a connection back to the server so you can push changes later.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Git Bash (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the repository by running this command:
git clone [[CLONE_URL="your Azure Repos HTTPS clone URL"]]
Seeing a sign-in prompt?
Git Credential Manager will open a browser window or prompt you to sign in with your Microsoft account. This is expected. Sign in with the same account you used for Azure DevOps.
You may also see a warning that you cloned an empty repository. This is normal because there are no files in it yet.
- Move into the cloned repository folder by running:
cd my-cicd-app
Clone failed or command not found?
If you see git: command not found, you need to install Git. Download it from git-scm.com. On Windows, make sure to install Git for Windows which includes Git Credential Manager.
If authentication fails, ensure you are signing in with the same Microsoft account you used to create the Azure DevOps organization. Help me fix my Git authentication with Azure Repos.
Create the application files
Your repository is empty. You'll create a minimal Express web application with three files: a package manifest, a server, and a test.
- Create a new file called package.json in the my-cicd-app folder with the following content:
{
"name": "my-cicd-app",
"version": "1.0.0",
"description": "A simple Node.js app for Azure CI/CD",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node index.test.js"
},
"dependencies": {
"express": "4.21.2"
}
}
What does this file do?
The package.json file tells Node.js about your project. It defines two scripts: start to run the app and test to run the test file.
It also lists Express as a dependency. The pipeline will run npm install to download Express automatically during the build.
You won't see a change yet. The next file uses this configuration.
- Create a new file called index.js in the same folder with the following content:
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 Pipeline!");
});
if (require.main === module) {
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
}
module.exports = app;
What does this file do?
This is your web server. It uses Express to respond to HTTP requests with a welcome message.
The process.env.PORT || 3000 line means the app uses whatever port Azure App Service assigns in production, or port 3000 locally. The module.exports = app line allows the test file to import the app without starting the server.
You won't see a change yet. The next file tests this server.
- Create a new file called index.test.js in the same folder with the following content:
const app = require("./index");
const http = require("http");
const server = app.listen(0, () => {
const port = server.address().port;
http.get(`http://localhost:${port}/`, (res) => {
let data = "";
res.on("data", (chunk) => { data += chunk; });
res.on("end", () => {
if (res.statusCode === 200 && data.includes("Hello from Azure DevOps")) {
console.log("TEST PASSED: App responds correctly");
server.close(() => process.exit(0));
} else {
console.error("TEST FAILED: Unexpected response");
server.close(() => process.exit(1));
}
});
});
});
What does this file do?
This is a simple test that starts the Express app on a random port, sends an HTTP GET request to it, and checks that the response contains the expected welcome message.
If the test passes, the script exits with code 0 (success). If it fails, it exits with code 1 (failure). The pipeline uses this exit code to determine whether the build passes or fails.
- Confirm all three files exist by running:
ls
You should see index.js, index.test.js, and package.json in the output.
Missing a file?
Make sure you saved each file inside the my-cicd-app folder (the one you cloned), not in a parent directory. Check that each filename matches exactly, including the .js or .json extension. Help me find my missing files.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before continuing.
ⓧ I'd like to double check the full code
Here are all three files for reference:
{
"name": "my-cicd-app",
"version": "1.0.0",
"description": "A simple Node.js app for Azure CI/CD",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node index.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 Pipeline!");
});
if (require.main === module) {
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
}
module.exports = app;
const app = require("./index");
const http = require("http");
const server = app.listen(0, () => {
const port = server.address().port;
http.get(`http://localhost:${port}/`, (res) => {
let data = "";
res.on("data", (chunk) => { data += chunk; });
res.on("end", () => {
if (res.statusCode === 200 && data.includes("Hello from Azure DevOps")) {
console.log("TEST PASSED: App responds correctly");
server.close(() => process.exit(0));
} else {
console.error("TEST FAILED: Unexpected response");
server.close(() => process.exit(1));
}
});
});
});
Push your code to Azure Repos
Your files are ready locally. Now you need to commit them to Git and push them up to Azure Repos so the pipeline can access them.
- Stage all your files, commit them, and push to the main branch by running these commands:
git add .
git commit -m "Add Node.js Express application"
git push origin main
What do these commands do?
- git add . stages all new and changed files in the current directory.
- git commit -m saves a snapshot of your staged files with a descriptive message.
- git push origin main uploads your local commits to the main branch in Azure Repos.
- Switch back to Azure DevOps in your browser.
- Click Repos in the left sidebar, then click Files.
You should see index.js, index.test.js, and package.json listed in the repository.
Push rejected or files not showing up?
If the push is rejected, make sure you are inside the my-cicd-app folder when running the commands. Run pwd to check your current directory.
If authentication fails during push, Git Credential Manager should prompt you to sign in again. Use the same Microsoft account you used for Azure DevOps. Help me push my code to Azure Repos.
Your application code is now safely stored in Azure Repos. Next up, you'll create the pipeline that automatically builds and deploys this app every time you push a change.
Create the CI/CD Pipeline
Your application code is now in Azure Repos, ready to be built and deployed automatically.
In this step, you will create the pipeline itself. Azure Pipelines uses a YAML file called azure-pipelines.yml that lives alongside your code and defines exactly what happens on every push.
In this step, get ready to:
- Create a new YAML pipeline through the Azure DevOps wizard.
- Write a Build stage that installs dependencies, runs tests, and publishes an artifact.
- Write a Deploy stage that sends the artifact to Azure App Service.
Create a new YAML pipeline
Azure DevOps provides a creation wizard that generates a starter azure-pipelines.yml file in your repository. You will replace the starter content with your own multi-stage pipeline.
- In your Azure DevOps project, select Pipelines from the left navigation menu.
- Select New pipeline (or Create pipeline if this is the first pipeline in the project).
- Select Azure Repos Git as your code source.
- Select your my-cicd-app repository.
- On the Configure tab, select Starter pipeline.
The editor opens with some default YAML content. You will replace all of it in the next substep.
What is a YAML pipeline?
A YAML pipeline defines your entire build and deployment process as code, stored right next to your application. This is called pipeline-as-code.
The hierarchy is Pipeline > Stages > Jobs > Steps. Stages run sequentially by default, and each stage starts only after the preceding one completes.
Write the Build stage
The Build stage installs Node.js, pulls in your dependencies, runs your tests, and packages the result as a pipeline artifact for later stages to use.
- Select all the starter content in the editor and replace it with the following code:
trigger:
- main
pool:
vmImage: "ubuntu-latest"
stages:
- stage: Build
displayName: "Build Stage"
jobs:
- job: BuildJob
displayName: "Build and Test"
steps:
# Install the correct Node.js version
- task: NodeTool@0
inputs:
versionSpec: "20.x"
displayName: "Install Node.js 20.x"
# Pull in project dependencies
- script: npm install
displayName: "Install dependencies"
# Run the test suite
- script: npm test
displayName: "Run tests"
# Package the app for the Deploy stage
- task: PublishPipelineArtifact@1
inputs:
targetPath: "$(System.DefaultWorkingDirectory)"
artifact: "app"
publishLocation: "pipeline"
displayName: "Publish artifact"
What does this code do?
- trigger: - main tells Azure Pipelines to run this pipeline every time code is pushed to the main branch.
- pool: vmImage: "ubuntu-latest" runs the pipeline on a Microsoft-hosted Linux agent.
- NodeTool@0 installs Node.js 20.x on the agent so your app can build.
- PublishPipelineArtifact@1 packages your working directory as an artifact named app so later stages can download it.
You will not save yet. The Deploy stage comes next.
Write the Deploy stage
The Deploy stage downloads the artifact from the Build stage and pushes it to your Azure App Service. It depends on the Build stage, so it only runs after a successful build.
- Place your cursor at the very end of the Build stage code (after the last line you just pasted).
- Add the following Deploy stage below the Build stage:
- stage: Deploy
displayName: "Deploy Stage"
dependsOn: Build
jobs:
- job: DeployJob
displayName: "Deploy to Azure App Service"
steps:
# Download the artifact from the Build stage
- task: DownloadPipelineArtifact@2
inputs:
buildType: "current"
artifactName: "app"
targetPath: "$(System.DefaultWorkingDirectory)"
displayName: "Download artifact"
# Deploy to Azure App Service
- task: AzureWebApp@1
inputs:
azureSubscription: "azure-service-connection"
appType: "webAppLinux"
appName: "[[APP_NAME="your-app-service-name"]]"
package: "$(System.DefaultWorkingDirectory)"
runtimeStack: "NODE|20-lts"
displayName: "Deploy to Azure App Service"
What does this code do?
- dependsOn: Build ensures the Deploy stage waits for a successful Build before starting.
- DownloadPipelineArtifact@2 retrieves the app artifact that was published in the Build stage.
- AzureWebApp@1 deploys your app to Azure App Service using a service connection called azure-service-connection. You will create this connection in the next step.
- appName must match the unique name you gave your App Service in Step 1.
- Select Save and run at the top right of the editor.
- Leave the commit message as-is and select Save and run again to confirm.
This commits azure-pipelines.yml to your main branch and immediately triggers a pipeline run.
Seeing the Deploy stage fail? Don't freak out
The Build stage should complete successfully, but the Deploy stage will fail. This is expected.
The pipeline references a service connection called azure-service-connection that does not exist yet. You will create it in the next step.
What is a pipeline artifact?
A pipeline artifact is a package of files produced by one stage and consumed by another. It allows the Deploy stage to use the exact same files that were tested in the Build stage, without rebuilding.
✔️ Awesome, I've got everything!
Great. Double check you can see azure-pipelines.yml in your repository's file list in Azure Repos.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file:
trigger:
- main
pool:
vmImage: "ubuntu-latest"
stages:
- stage: Build
displayName: "Build Stage"
jobs:
- job: BuildJob
displayName: "Build and Test"
steps:
- task: NodeTool@0
inputs:
versionSpec: "20.x"
displayName: "Install Node.js 20.x"
- script: npm install
displayName: "Install dependencies"
- script: npm test
displayName: "Run tests"
- task: PublishPipelineArtifact@1
inputs:
targetPath: "$(System.DefaultWorkingDirectory)"
artifact: "app"
publishLocation: "pipeline"
displayName: "Publish artifact"
- stage: Deploy
displayName: "Deploy Stage"
dependsOn: Build
jobs:
- job: DeployJob
displayName: "Deploy to Azure App Service"
steps:
- task: DownloadPipelineArtifact@2
inputs:
buildType: "current"
artifactName: "app"
targetPath: "$(System.DefaultWorkingDirectory)"
displayName: "Download artifact"
- task: AzureWebApp@1
inputs:
azureSubscription: "azure-service-connection"
appType: "webAppLinux"
appName: "your-app-name-here"
package: "$(System.DefaultWorkingDirectory)"
runtimeStack: "NODE|20-lts"
displayName: "Deploy to Azure App Service"
Build stage failing too?
- If you see a "No hosted parallelism has been purchased or granted" error, your free parallel job grant has not been approved yet. Check Organization Settings > Pipelines > Parallel jobs for the status of your request.
- If the YAML shows red squiggles or a syntax error, check that your indentation uses spaces (not tabs) and matches the code above exactly. YAML is sensitive to indentation.
Help me debug my Azure DevOps pipeline YAML errors
Your pipeline YAML is committed and the Build stage proves your code compiles and tests pass. Next up, you will create the service connection so the Deploy stage can reach your Azure App Service.
Connect Azure DevOps to Azure
In the previous step, you wrote a complete multi-stage pipeline with Build and Deploy stages. There is one problem: the Deploy stage references a service connection called azure-service-connection that does not exist yet.
A service connection is a secure link between Azure DevOps and your Azure subscription. It lets the pipeline deploy to your App Service without you pasting credentials into the YAML file. In this step, you will create that connection, update your pipeline with your real App Service name, and watch the full pipeline deploy your app.
In this step, get ready to:
- Create a service connection between Azure DevOps and your Azure subscription.
- Update the pipeline YAML with your actual App Service name.
- Run the pipeline and deploy your app to Azure App Service.
Create an Azure Resource Manager service connection
The AzureWebApp@1 task in your pipeline needs permission to deploy to your Azure subscription. You grant that permission by creating a service connection in Project Settings.
- In your Azure DevOps project, click Project settings at the bottom-left of the sidebar.
- In the left menu under Pipelines, select Service connections.
- Click New service connection.
- Select Azure Resource Manager and click Next.
- Select Service principal (automatic) and click Next.
- For Scope level, select Subscription.
- Select your Azure subscription from the dropdown.
- Select the resource group that contains your App Service (the one you created in Step 1).
- In the Service connection name field, enter azure-service-connection.
- Check the Grant access permission to all pipelines checkbox.
- Click Save.
What just happened?
Azure DevOps created a service principal in your Microsoft Entra tenant and assigned it the Contributor role on your selected resource group. Your pipeline can now deploy to any resource in that group without you managing secrets manually.
The connection name azure-service-connection must match the azureSubscription value in your YAML file exactly. That is how the pipeline knows which credentials to use.
Prompted to sign in to Azure?
Azure DevOps may ask you to authenticate with your Azure account during service connection creation. Sign in with the same Microsoft account that owns your Azure subscription.
If you see a permissions error, make sure you have Owner or Contributor access on the Azure subscription. Help me fix my service connection permissions.
Update the pipeline with your App Service name
Your pipeline YAML currently has a placeholder value for appName. You need to replace it with the actual name of the Azure App Service you created in Step 1.
- In the left sidebar of Azure DevOps, click Pipelines.
- Select your pipeline from the list.
- Click Edit in the top-right corner to open the YAML editor.
- Scroll down to the Deploy stage and find this line:
appName: "your-app-name-here"
- Replace your-app-name-here with the exact name of your Azure App Service, so the line reads:
appName: "[[APP_SERVICE_NAME=\"my-cicd-app-yourname\"]]"
Where do I find my App Service name?
Your App Service name is whatever you chose in Step 1 when creating the Web App in the Azure Portal. It is the first part of your app's URL.
For example, if your URL is my-cicd-app-alex.azurewebsites.net, then your app name is my-cicd-app-alex.
Save and run the pipeline
With the service connection in place and the correct app name in your YAML, you can now trigger a deployment.
- In the YAML editor, click Save.
- In the commit dialog, keep Commit directly to the main branch selected.
- Click Save to commit the change.
Why does saving trigger the pipeline?
Your azure-pipelines.yml has trigger: - main at the top. Every commit to the main branch triggers a new pipeline run automatically. This is the CI trigger in action.
- Click Pipelines in the left sidebar to see the triggered run.
- Click into the run to watch both stages execute.
- Wait for the Build Stage to show a green checkmark, then watch Deploy Stage complete.
Both stages should show green checkmarks when the pipeline finishes.
What is happening in the Deploy stage?
The Deploy stage downloads the artifact published by the Build stage, then uses the AzureWebApp@1 task to push your code to Azure App Service. Azure App Service detects the package.json file, installs dependencies, and starts your Express server automatically.
Deploy stage failed?
Check these common causes:
- The appName in your YAML does not match your App Service name exactly. Check for typos or extra spaces.
- The service connection name in your YAML (azure-service-connection) does not match the name you gave the connection in Project Settings.
- You may need to authorize the pipeline to use the service connection. Click the failed stage, look for an authorization prompt, and click Permit.
Help me debug my failed Deploy stage.
✔️ Awesome, I've got everything!
Great. Double check you have saved your azure-pipelines.yml and that both pipeline stages are green.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file at this point. Compare it to what you have in the YAML editor.
trigger:
- main
pool:
vmImage: "ubuntu-latest"
stages:
- stage: Build
displayName: "Build Stage"
jobs:
- job: BuildJob
displayName: "Build and Test"
steps:
- task: NodeTool@0
inputs:
versionSpec: "20.x"
displayName: "Install Node.js 20.x"
- script: npm install
displayName: "Install dependencies"
- script: npm test
displayName: "Run tests"
- task: PublishPipelineArtifact@1
inputs:
targetPath: "$(System.DefaultWorkingDirectory)"
artifact: "app"
publishLocation: "pipeline"
displayName: "Publish artifact"
- stage: Deploy
displayName: "Deploy Stage"
dependsOn: Build
jobs:
- job: DeployJob
displayName: "Deploy to Azure App Service"
steps:
- task: DownloadPipelineArtifact@2
inputs:
buildType: "current"
artifactName: "app"
targetPath: "$(System.DefaultWorkingDirectory)"
displayName: "Download artifact"
- task: AzureWebApp@1
inputs:
azureSubscription: "azure-service-connection"
appType: "webAppLinux"
appName: "[[APP_SERVICE_NAME=\"my-cicd-app-yourname\"]]"
package: "$(System.DefaultWorkingDirectory)"
runtimeStack: "NODE|20-lts"
displayName: "Deploy to Azure App Service"
Your pipeline is fully connected and deploying automatically. Next up, you will visit your live app and test the CI trigger by pushing a code change.
Verify Your Live Deployment and CI Trigger
Your pipeline has run and both stages show green. Now it's time to confirm the end-to-end result: a live application accessible to anyone with the URL.
In this step, you'll verify the deployed app is live, then prove the CI trigger works by pushing a code change and watching it deploy automatically.
In this step, get ready to:
- Review the pipeline run stages and logs.
- Visit your live deployed application.
- Test the CI trigger with a code change.
Check the pipeline run
The pipeline you triggered in the previous step has completed. Let's review what happened at each stage.
- In your Azure DevOps project, click Pipelines in the left sidebar.
- Click on the most recent pipeline run (it should show a green checkmark).
- You should see two stages displayed horizontally: Build Stage and Deploy Stage, both with green checkmarks.
- Click on the Build Stage to expand its job, then click Build and Test to see the step-by-step logs.
What should I see in the logs?
The Build stage logs show each step running in sequence: Node.js installation, npm install downloading Express, npm test printing "TEST PASSED", and finally the artifact being published.
The Deploy stage logs show the artifact being downloaded and then deployed to your App Service. This is the pipeline hierarchy in action: Pipeline > Stages > Jobs > Steps.
Visit your live app
The Deploy stage pushed your application to Azure App Service. Your app is now live on the internet with its own URL.
- 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 Pipeline!" displayed in your browser.
Where do I find my App Service URL?
If you've forgotten your app name, return to the Azure Portal. Navigate to your App Service resource and find the URL on the Overview page. It follows the pattern https://<your-app-name>.azurewebsites.net.
Seeing an error page instead?
If you see a default Azure placeholder page, your deployment may still be in progress. Wait 1-2 minutes and refresh.
If you see an application error, return to Azure DevOps and check the Deploy stage logs for a failed step. The most common issue is a mismatch between the appName in your YAML and your actual App Service name.
Help me debug my App Service deployment
Test the CI trigger
The real power of CI/CD is that every code push deploys automatically. Let's prove it works by making a change and watching the pipeline fire.
- In your local repository, open index.js.
- Find this line:
res.send("Hello from Azure DevOps CI/CD Pipeline!");
- Replace the message with something new, so the line looks like this:
res.send("Hello from Azure DevOps CI/CD Pipeline! Deployed automatically.");
- Save index.js.
- Commit and push your change by running these commands:
git add .
git commit -m "Update welcome message"
git push
What happens next?
Your azure-pipelines.yml has trigger: - main configured at the top. This tells Azure DevOps to start a new pipeline run every time code is pushed to the main branch.
The pipeline will run through both stages again: Build (install, test, publish artifact) and Deploy (download artifact, push to App Service). No manual intervention needed.
- Switch back to Azure DevOps in your browser.
- Click Pipelines in the left sidebar.
- You should see a new pipeline run appear (it may already be in progress with a blue spinning icon).
- Wait for both stages to complete with green checkmarks.
- Refresh your app URL in the browser. You should see your updated welcome message.
Don't see a new pipeline run?
Make sure you pushed to the main branch (not a feature branch). Run git branch to confirm you're on main.
If the push succeeded but no run appears, check that your azure-pipelines.yml trigger section says - main (not a different branch name).
Help me figure out why my CI trigger isn't firing
That's it. You just proved the full CI/CD cycle: change code, push, and the pipeline handles everything else.
Secret mission
Add a Test Stage with Quality Gates
Your pipeline currently goes straight from Build to Deploy. In production teams, code passes through multiple validation gates before reaching users. Can you add a dedicated Test stage and a branch-based deployment condition so that only code on main ever gets deployed?
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 F1 Free tier and the Azure DevOps free tier, so there are no ongoing costs as long as you stay within quotas.
Resources you used:
- Azure App Service (F1 Free tier) running your Node.js app.
- Azure DevOps project (includes Repos, Pipelines, and the service connection).
- Local Git clone of the repository on your machine.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your pipeline.
- The F1 Free tier costs $0. It consumes 60 CPU minutes per day, which resets at midnight UTC.
- Azure DevOps free tier gives you 1 parallel job and up to 30 hours per month of pipeline time at no charge.
- Your local files remain on your machine and take up minimal disk space.
Pause - I'll come back to this later
Stop the App Service to prevent any CPU usage. Your pipeline will fail to deploy until you restart it, but your code and configuration remain intact.
- In the Azure Portal, search for and open App Services.
- Select your App Service from the list.
- Click Stop in the toolbar at the top of the overview page.
- When you are ready to resume, return to the same page and click Start.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is irreversible.
Delete the Azure resource group (removes the App Service)
- In the Azure Portal, search for and open Resource groups.
- Select the resource group that contains your App Service.
- Click Delete resource group.
- Type the resource group name to confirm, then click Delete.
Delete the Azure DevOps project
- Sign in to your organization at dev.azure.com.
- Click Organization settings in the bottom-left corner.
- Select Projects, then check the box next to your project.
- Click Delete.
- Type the project name to confirm, then click Delete in the popup.
Delete the local repository
- Delete the local clone from your machine by running:
macOS
rm -rf ~/Desktop/my-cicd-app
Windows
Remove-Item -Recurse -Force ~\Desktop\my-cicd-app
Nice Work!
Nice Work!
You just built a professional CI/CD pipeline from scratch using Azure DevOps. Every time you push code to main, your pipeline automatically builds, tests, and deploys your app to a live URL.
You've learned how to:
- Created a multi-stage YAML pipeline with Build and Deploy stages that run sequentially in Azure Pipelines.
- Configured a service connection between Azure DevOps and Azure, giving your pipeline permission to deploy to Azure App Service.
- Verified the CI trigger by pushing a code change and watching your app redeploy automatically within minutes.
- Added a dedicated Test stage with branch-based deployment conditions so only main branch pushes reach production.
Ready to quiz yourself?