CI/CD Pipeline with GitHub Actions
Automate builds, tests, and deploys to Azure App Service with GitHub Actions.
Introduction
30 Second Summary
Every time you fix a bug or add a feature, someone has to get that code onto a live server. Most professional teams automate this entire process so that a simple code push triggers testing and deployment without any manual steps.
In this project, you will build an automated deployment pipeline using GitHub Actions and Azure App Service. You'll push code to GitHub and watch it automatically test, build, and deploy to a live URL.
What You'll Build
A complete CI/CD pipeline that automatically deploys a Node.js Express application to Azure App Service on every push to your main branch.
By the end of this project, you'll have:
- A Node.js Express API with a health check endpoint and an automated test.
- A GitHub Actions workflow file that builds, tests, and deploys your code automatically.
- A live Azure App Service running your app on a public URL with secure credential management.
- Secret Mission: A staging environment with pull request triggers so you can test deployments before they go live.
Are there any prerequisites?
You'll need basic familiarity with Git (clone, commit, push). No prior CI/CD or Azure experience is required.
Before We Start
Before you write a single line of pipeline code, you need a solid foundation of tools installed and accounts ready to go.
This step gets your local machine and cloud accounts prepared so you can focus on building your CI/CD pipeline without interruption.
In this step, get ready to:
- Install and verify Node.js and npm.
- Install Git and confirm your GitHub account.
- Create a free Azure account for hosting your app.
Install Node.js and verify
Your app runs on Node.js, a JavaScript runtime that lets you execute JavaScript outside of a browser. The project requires Node.js version 20 or higher.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Node.js is already installed by running this command:
node --version
✔️ I see version 20 or higher
If you see a version number starting with v20 or above (like v20.x.x, v22.x.x, or v24.x.x), Node.js is ready.
- Also verify npm is available by running:
npm --version
You should see a version number printed. Both tools are ready to go.
ⓧ I see an older version
If you see a version below v20 (like v18.x.x or v16.x.x), you need to upgrade.
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer for your operating system.
- Run the installer and accept the default options.
- Close and reopen your terminal after installation.
- Run node --version again to confirm you see v20 or higher.
Why do I need to restart my terminal?
Your terminal loads the system PATH when it first opens. Installing Node.js updates the PATH, but your existing terminal session still has the old PATH cached. Closing and reopening the terminal picks up the new path to the updated Node.js binary.
ⓧ Command not found
Node.js is not installed on your machine yet. Let's install it now.
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer for your operating system.
- Run the installer and accept the default options.
- Close and reopen your terminal after installation.
- Verify the installation by running:
node --version
You should see a version number starting with v20 or higher.
- Also verify npm is available by running:
npm --version
You should see a version number printed. Both tools are now installed.
Still seeing an old version after reinstalling?
On macOS, the system may have an older Node.js in /usr/local/bin that shadows the new installation. Run which node to check which binary your terminal is using. If it points to an unexpected location, you may need to update your PATH or use a version manager like nvm.
Help me fix my Node.js version issue
Install Git and configure GitHub
Git is a version control system that tracks changes to your code. GitHub is the hosting platform where your repository lives and where GitHub Actions runs your pipeline.
- Check if Git is already installed by running this command:
git --version
✔️ I see a version number
If you see output like git version 2.x.x, Git is installed and ready.
ⓧ Command not found
Git is not installed yet. Here is how to install it:
macOS
- Run the following command to install Git via Xcode Command Line Tools:
xcode-select --install
A dialog will appear asking you to confirm the installation. Click Install and wait for it to complete.
Windows
- Go to https://git-scm.com/install/windows in your browser.
- Click the download link for the latest x64 version.
- Run the installer and accept the default options.
- Close and reopen your terminal after installation.
- After installation, verify by running:
git --version
You should now see a version number printed.
- Go to https://github.com and sign in to your account.
- If you do not have a GitHub account yet, click Sign up and follow the prompts to create a free account.
Why a public repository?
GitHub Actions provides unlimited free minutes for public repositories. Private repositories get 2,000 minutes per month on the free plan. This project recommends a public repository so you can run your pipeline without worrying about usage limits.
Create an Azure free account
Azure is Microsoft's cloud platform. You will deploy your app to Azure App Service, which can host web applications. The free tier costs nothing for this project.
- Go to https://azure.microsoft.com/free in your browser.
- Click Start free (or the equivalent sign-up button).
- Sign in with a Microsoft account, or create a new one if prompted.
- Complete the sign-up process, which includes verifying your identity with a phone number and a credit card.
Will I be charged?
The Azure free account includes 12 months of free services and $200 credit for 30 days. This project uses the Free F1 tier of App Service, which does not consume any of your credits. You will not be charged for completing this project.
- Once signed up, go to https://portal.azure.com and confirm you can see the Azure dashboard.
Can't access the Azure Portal?
If you see an error about your subscription, make sure you completed the free account sign-up fully (including credit card verification). Azure requires a card on file even for the free tier, but you will not be charged for this project.
Help me troubleshoot my Azure account setup
Your tools are installed and your accounts are ready. Next up, you will create the Node.js app and push it to GitHub.
Create the Node.js App and GitHub Repository
Your development tools are installed and ready to go. Now you need the actual application that your pipeline will build, test, and deploy.
In this step, you will create a simple Express API with a health check endpoint, write an automated test for it, and push everything to GitHub so your pipeline has code to work with.
In this step, get ready to:
- Initialize a Node.js project and build an Express API.
- Write an automated test for your endpoints.
- Push your code to a GitHub repository.
Initialize the project and install Express
Every Node.js project starts with a folder and a package.json file that tracks dependencies and scripts. You will scaffold the project, install Express, and then write the application code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir github-actions-azure-demo
cd github-actions-azure-demo
- Initialize a new Node.js project by running:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. This file is the manifest for your project. It tracks which packages your app depends on and defines scripts you can run.
- Install Express by running:
npm install express
- Confirm the installation succeeded by running:
ls node_modules | head -5
You should see a list of package folders inside node_modules. This confirms Express and its dependencies were installed.
Seeing permission errors?
If npm install fails with permission errors, make sure you are inside the github-actions-azure-demo folder (not in a system directory). Run pwd to check your current location.
help me fix npm permission errors
Now open the project in your code editor so you can create the application files.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the github-actions-azure-demo folder on your Desktop, then click Open.
You should see your project in the Explorer sidebar with package.json and node_modules listed.
- Create a new file called app.js by right-clicking in the Explorer sidebar and selecting New File.
- Name the file app.js and paste in the following code:
const express = require('express');
const app = express();
// Health check endpoint - returns server status
app.get('/', (req, res) => {
res.json({
status: 'healthy',
message: 'GitHub Actions CI/CD Pipeline is working!',
timestamp: new Date().toISOString()
});
});
// Status endpoint - returns app metadata
app.get('/api/status', (req, res) => {
res.json({
app: 'github-actions-azure-demo',
version: '1.0.0',
environment: process.env.NODE_ENV || 'development'
});
});
module.exports = app;
What does this code do?
This file defines your Express application with two endpoints.
- The / route returns a JSON health check confirming the server is running.
- The /api/status route returns metadata like the app name, version, and current environment.
- Exporting app with module.exports lets other files import and use this app (for testing and for the server entry point).
- Save app.js.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({
status: 'healthy',
message: 'GitHub Actions CI/CD Pipeline is working!',
timestamp: new Date().toISOString()
});
});
app.get('/api/status', (req, res) => {
res.json({
app: 'github-actions-azure-demo',
version: '1.0.0',
environment: process.env.NODE_ENV || 'development'
});
});
module.exports = app;
- Create another new file called server.js in the same way (right-click in Explorer, select New File).
- Name it server.js and paste in the following code:
const app = require('./app');
// Use the PORT environment variable (set by Azure) or default to 3000
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Why separate app.js and server.js?
Separating the app definition from the server startup lets you import app in your tests without actually starting a persistent server process. The PORT environment variable is set by Azure App Service at runtime so your app listens on the correct port.
- Save server.js.
✔️ Awesome, I've got everything!
Great. Make sure server.js is saved.
ⓧ I'd like to double check the full code
const app = require('./app');
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Add a test
Automated tests are what make a CI/CD pipeline useful. Without tests, the pipeline just deploys code blindly. With tests, the pipeline catches bugs before they reach production.
- Create a new file called app.test.js in the Explorer sidebar.
- Paste in the following test code:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const app = require('./app');
let server;
let baseUrl;
describe('App endpoints', () => {
// Test the health check endpoint
it('GET / returns healthy status', async () => {
server = app.listen(0);
const port = server.address().port;
baseUrl = `http://localhost:${port}`;
const response = await fetch(`${baseUrl}/`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.status, 'healthy');
assert.ok(data.timestamp);
server.close();
});
// Test the status metadata endpoint
it('GET /api/status returns app info', async () => {
server = app.listen(0);
const port = server.address().port;
baseUrl = `http://localhost:${port}`;
const response = await fetch(`${baseUrl}/api/status`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.app, 'github-actions-azure-demo');
assert.strictEqual(data.version, '1.0.0');
server.close();
});
});
What does this test do?
This file uses Node.js's built-in test runner (node:test) so you do not need any external testing library.
- Each test starts the app on a random port (app.listen(0)), sends a real HTTP request, and checks the response.
- The first test verifies the / endpoint returns a 200 status with 'healthy' in the body.
- The second test verifies /api/status returns the correct app name and version.
- Save app.test.js.
✔️ Awesome, I've got everything!
Great. Confirm app.test.js is saved.
ⓧ I'd like to double check the full code
const { describe, it } = require('node:test');
const assert = require('node:assert');
const app = require('./app');
let server;
let baseUrl;
describe('App endpoints', () => {
it('GET / returns healthy status', async () => {
server = app.listen(0);
const port = server.address().port;
baseUrl = `http://localhost:${port}`;
const response = await fetch(`${baseUrl}/`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.status, 'healthy');
assert.ok(data.timestamp);
server.close();
});
it('GET /api/status returns app info', async () => {
server = app.listen(0);
const port = server.address().port;
baseUrl = `http://localhost:${port}`;
const response = await fetch(`${baseUrl}/api/status`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.app, 'github-actions-azure-demo');
assert.strictEqual(data.version, '1.0.0');
server.close();
});
});
Now you need to update package.json with the correct scripts and project metadata so the test command works.
- Open package.json in the Explorer sidebar.
- Replace the entire contents of package.json with the following:
{
"name": "github-actions-azure-demo",
"version": "1.0.0",
"description": "A Node.js app deployed via GitHub Actions to Azure App Service",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "node --test app.test.js"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {},
"engines": {
"node": ">=20.0.0"
}
}
What changed in package.json?
- The start script tells Azure how to launch your app in production.
- The test script uses Node's built-in test runner (node --test) to run app.test.js without needing Jest or Mocha.
- The engines field documents that this project requires Node 20 or higher.
- Save package.json.
✔️ Awesome, I've got everything!
Great. Make sure package.json is saved with the updated content.
ⓧ I'd like to double check the full code
{
"name": "github-actions-azure-demo",
"version": "1.0.0",
"description": "A Node.js app deployed via GitHub Actions to Azure App Service",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "node --test app.test.js"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {},
"engines": {
"node": ">=20.0.0"
}
}
Now verify that your tests pass locally.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, top-left of keyboard).
- Run the tests by running:
npm test
You should see output showing both tests passing with green checkmarks or pass indicators. The output will display your two test names and a summary showing 2 tests passed.
Tests failing?
If you see errors about missing modules, make sure app.js and app.test.js are saved in the root of your github-actions-azure-demo folder (not inside a subfolder).
If you see a syntax error, double-check that your package.json has valid JSON (no trailing commas, all quotes are double quotes).
help me debug why npm test is failing
Push to GitHub
Your code lives locally right now. For GitHub Actions to build and deploy it, the code needs to live in a GitHub repository.
- Open your browser and go to github.com/new to create a new repository.
- Enter github-actions-azure-demo in the Repository name field.
- Select Public for the repository visibility.
- Do NOT check any of the initialization boxes (no README, no .gitignore, no license).
- Click Create repository.
Why make the repo public?
Public repositories get unlimited free GitHub Actions minutes. Private repositories are limited to 2,000 minutes per month on the free plan. Keeping it public ensures you will not hit any billing limits during this project.
After creating the repository, GitHub shows a Quick Setup page with the repository URL. You will use this URL in the next commands.
- Switch back to your terminal in VS Code.
- Initialize Git, add your files, commit, and push by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/github-actions-azure-demo.git"]]
git push -u origin main
What do these commands do?
- git init turns your folder into a Git repository.
- git add . stages all files for the first commit.
- git branch -M main ensures your default branch is named main (required for the workflow trigger later).
- git remote add origin connects your local repo to the GitHub repository you just created.
- git push -u origin main uploads your code to GitHub.
- Go to your repository page on github.com.
- Confirm you see your files (app.js, server.js, app.test.js, package.json) listed in the repository.
Push rejected or authentication failed?
If Git asks for a password and rejects it, GitHub no longer accepts passwords for HTTPS pushes. You need a Personal Access Token or SSH key configured. Check your GitHub authentication settings under Settings then Developer settings then Personal access tokens.
help me authenticate git push to GitHub
Your application code is live on GitHub. Next up, you will create the Azure App Service that will host your app in the cloud.
Create the Azure App Service
Your code is on GitHub, but it needs a home on the internet. Right now, pushing code to your repository does nothing visible to the outside world.
In this step, you will create an Azure App Service to host your app. You will also download its publish profile and store it as a GitHub secret so your workflow can authenticate with Azure automatically.
In this step, get ready to:
- Create an Azure App Service with the Free F1 pricing plan.
- Download the publish profile and store it as a GitHub repository secret.
Create an App Service in Azure Portal
Azure App Service is a fully managed hosting platform for web apps. You are going to create one that runs your Node.js Express app on the Free tier.
- Return to the Azure Portal at portal.azure.com.
- In the top search bar, type app services.
- Select App Services under the Services heading.
- Click Create, then select Web App.
You are now in the Create Web App wizard. Fill out the form with these settings:
- Under Resource group, click Create new. Enter a name like github-actions-rg and click OK.
- Under Name, type a globally unique name for your web app. This name becomes your app's URL, so it must be unique across all of Azure. Use only letters, numbers, and hyphens.
- Under Publish, make sure Code is selected.
- Under Runtime stack, select Node 20 LTS.
- Under Operating System, select Linux.
- Under Region, choose the region closest to your physical location.
- Under Pricing plan, select Free F1 from the dropdown. If you do not see it, click Create new to create a new App Service plan, then select Free F1.
Why does my app name need to be unique?
Your app name becomes part of the public URL for your site. Azure uses the format https://your-app-name.azurewebsites.net. Since this is a real internet address, no two apps in Azure can share the same name.
Pick something memorable that includes your name or initials, like nextwork-cicd-jane. You will reference this name again when configuring the workflow.
- Click Review + create at the bottom of the page.
- After validation runs, click Create.
- Wait for the deployment to complete. This may take a minute or two.
- Once deployment is complete, click Go to resource.
You should now see the Overview page for your new App Service. The default domain is shown near the top of the page.
What is a Resource Group?
A Resource Group is a logical container in Azure that holds related resources. It does not cost anything on its own. Grouping resources together makes it easy to manage and delete them all at once when you are done with a project.
Don't see the Free F1 pricing option?
Some Azure subscriptions restrict the Free tier in certain regions. Try switching to a different region (like East US or West Europe) and check the pricing plan dropdown again.
If you still do not see it, make sure you are creating a Linux App Service plan. The Free F1 tier is available on Linux.
help me find the Free F1 pricing tier in Azure Portal
Download the publish profile and configure the GitHub secret
A publish profile is an XML file that contains the credentials Azure needs to accept deployments. You will download it and store it in GitHub so your workflow can deploy to Azure without you entering a password.
- On your App Service's Overview page, click Download publish profile in the top toolbar.
- A .PublishSettings file downloads to your computer. This file contains authentication credentials for your App Service.
- Open the downloaded file in a text editor (VS Code, Notepad, or TextEdit).
- Select all the contents of the file and copy them to your clipboard.
Why a publish profile instead of managed identity?
Azure supports multiple authentication methods for deployments. The publish profile approach is the simplest to set up. You download a file and paste it as a secret. The alternative (OpenID Connect with managed identities) requires configuring Azure Active Directory, federated credentials, and role assignments.
For a first CI/CD project, the publish profile gets you deploying quickly without extra infrastructure setup.
Now you will store the publish profile contents as a secret in your GitHub repository.
- Switch back to your GitHub repository in your browser.
- Click the Settings tab at the top of your repository page.
- In the left sidebar under Security, click Secrets and variables, then click Actions.
- Click the Secrets tab.
- Click New repository secret.
- In the Name field, type AZURE_WEBAPP_PUBLISH_PROFILE.
- In the Secret field, paste the entire contents of the publish profile file you copied earlier.
- Click Add secret.
You should see AZURE_WEBAPP_PUBLISH_PROFILE listed on the repository secrets page. The value is encrypted and hidden. GitHub will only expose it to workflows running in this repository.
What are GitHub Secrets?
GitHub Secrets are encrypted environment variables that your GitHub Actions workflows can access at runtime. They never appear in logs or in your repository code. This is the safe way to give your CI/CD pipeline access to external services without hardcoding credentials.
Seeing 'Secret name already exists'?
If you previously created a secret with the same name, click Update next to the existing secret to replace its value with the new publish profile contents.
help me configure the AZURE_WEBAPP_PUBLISH_PROFILE secret in GitHub
Your Azure infrastructure is ready and connected to GitHub. Next up, you will write the workflow file that tells GitHub Actions how to build, test, and deploy your app automatically.
Write the GitHub Actions Workflow
Your Azure App Service is ready and waiting for code. Now you need to create the automation that connects your GitHub repository to Azure.
GitHub Actions uses a YAML workflow file stored in your repository to define what happens on every push. You will write this file from scratch so you understand every line.
In this step, get ready to:
- Create the .github/workflows/ directory structure.
- Write the build job to install, test, and package your app.
- Write the deploy job to push the artifact to Azure.
Create the workflow file structure
GitHub Actions discovers workflow files by looking inside a special .github/workflows/ directory at the root of your repository. You need to create this directory and a deploy.yml file inside it.
- Create the workflow directory by running this command in your project's root folder:
mkdir -p .github/workflows
- Confirm the directory exists by running:
ls .github
You should see workflows listed in the output.
- Create a new file called deploy.yml inside .github/workflows/ and open it in your editor.
- Add the workflow metadata by pasting this code into deploy.yml:
# Name that appears in the Actions tab on GitHub
name: Build and Deploy to Azure
# Trigger this workflow on every push to the main branch
on:
push:
branches:
- main
# Environment variables available to all jobs
env:
AZURE_WEBAPP_NAME: [[AZURE_APP_NAME="your Azure App Service name"]]
AZURE_WEBAPP_PACKAGE_PATH: '.'
NODE_VERSION: '20'
What does this code do?
name sets the display name you will see in the GitHub Actions tab.
The on: push: branches: - main block tells GitHub to run this workflow every time someone pushes code to the main branch.
The env section defines variables that both jobs can reference. AZURE_WEBAPP_NAME must match the exact name of your App Service in Azure.
Not sure what your app name is?
Return to the Azure Portal and check the Overview page of your App Service. The name at the top is what you enter for AZURE_WEBAPP_NAME.
help me find my Azure App Service name
Configure the build job
The build job handles checking out your code, installing dependencies, running tests, and packaging the result as an artifact. This is the Continuous Integration half of your pipeline.
- In deploy.yml, add the following below the env section you just wrote:
jobs:
build:
runs-on: ubuntu-latest
steps:
# Pull your repository code onto the runner
- name: Checkout code
uses: actions/checkout@v4
# Install Node.js and enable npm dependency caching
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
# Install project dependencies
- name: Install dependencies
run: npm install
# Run the test suite to catch errors before deploying
- name: Run tests
run: npm test
# Package the build output for the deploy job
- name: Upload artifact for deployment
uses: actions/upload-artifact@v4
with:
name: node-app
path: .
What does this code do?
runs-on: ubuntu-latest tells GitHub to spin up a fresh Linux virtual machine for this job.
actions/checkout@v4 clones your repository onto that machine. actions/setup-node@v4 installs Node.js and caches your npm dependencies so future runs are faster.
actions/upload-artifact@v4 saves your entire project directory as a downloadable artifact. The deploy job will grab this artifact in the next stage.
You will not see a change yet. The next chunk adds the deploy job that completes the workflow.
Configure the deploy job
The deploy job takes the tested artifact from the build job and pushes it to your Azure App Service. This is the Continuous Deployment half of your pipeline.
- In deploy.yml, add the deploy job directly below the build job (at the same indentation level as build:):
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
# Grab the artifact created in the build job
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: node-app
# Deploy the artifact to Azure App Service
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
What does this code do?
needs: build makes the deploy job wait until the build job finishes successfully. If tests fail, the deploy never runs.
actions/download-artifact@v4 retrieves the packaged artifact from the build step. azure/webapps-deploy@v3 uses your publish profile secret to authenticate and deploy the code to Azure.
The environment block labels this deployment as production and outputs the live URL for easy access.
Why two separate jobs instead of one?
Separating build and deploy follows a professional pattern where each job has one responsibility. If the build fails, you get clear feedback that the issue is in your code, not in the deployment step.
This pattern is also transferable. Every CI/CD platform uses the same idea of passing artifacts between stages.
- Save deploy.yml.
Seeing YAML indentation errors?
YAML is sensitive to spaces. Make sure deploy: is indented with exactly two spaces under jobs:, the same level as build:. Each steps: item should be indented with six spaces from the left margin.
help me fix YAML indentation in my GitHub Actions workflow file
✔️ Awesome, I've got everything!
Great. Double check you have saved deploy.yml before continuing.
ⓧ I'd like to double check the full code
Here is the complete deploy.yml file for comparison:
name: Build and Deploy to Azure
on:
push:
branches:
- main
env:
AZURE_WEBAPP_NAME: [[AZURE_APP_NAME="your Azure App Service name"]]
AZURE_WEBAPP_PACKAGE_PATH: '.'
NODE_VERSION: '20'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Upload artifact for deployment
uses: actions/upload-artifact@v4
with:
name: node-app
path: .
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: node-app
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
- Commit and push the workflow file to GitHub by running these commands:
git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions workflow"
git push
What does this do?
Pushing this file to the main branch tells GitHub to look inside .github/workflows/ for any YAML files. It will pick up deploy.yml and start running the workflow immediately.
- Go to your GitHub repository page in your browser.
- Click the Actions tab at the top of the page.
You should see a workflow run named Build and Deploy to Azure starting or already in progress. This confirms GitHub discovered your workflow file.
Don't see a workflow run?
Make sure your push went through by checking the commit history on your repository's main page. The latest commit should say "Add GitHub Actions workflow".
Also confirm that deploy.yml appears under the .github/workflows/ directory in your repository's file list on GitHub.
my GitHub Actions workflow is not running after I pushed
Your CI/CD pipeline is now live. In the next step, you will watch it complete and confirm your app is deployed to Azure.
Trigger the Pipeline and Verify
Your workflow file is on GitHub, and the pipeline is already in motion. When you pushed to the main branch in the last step, GitHub Actions detected the workflow and started running it automatically.
In this step, you will watch the pipeline complete in real time, verify your app is live on Azure, and prove the full CI/CD cycle works by pushing a code change.
In this step, get ready to:
- Watch the pipeline complete in the GitHub Actions tab.
- Verify the live app responds on Azure App Service.
- Test the automated deployment cycle with a code change.
Watch the pipeline run in GitHub Actions
Your push to the main branch triggered the workflow automatically. The Actions tab in your repository is where you can watch every step execute in real time.
- Switch back to your GitHub repository in the browser.
- Click the Actions tab at the top of the repository page.
- In the left sidebar, click Build and Deploy to Azure to filter to your workflow.
- Click on the most recent workflow run (it shows your last commit message as the title).
- Watch the visualization graph display both the build and deploy jobs with status icons next to each job name.
What am I looking at?
The workflow run summary shows a visualization graph with each job as a node. An icon to the left of each job name indicates its status. Lines between jobs show dependencies.
The build job checks out your code, installs dependencies, runs your tests, and uploads the build artifact. The deploy job waits for build to finish (the needs: build line), downloads the artifact, and pushes it to Azure.
Wait for both jobs to complete. This typically takes 1-3 minutes.
✔️ Both jobs show green checkmarks
Your pipeline ran successfully. Both jobs completed with a success conclusion, meaning your tests passed and the deployment finished without errors.
ⓧ One of the jobs failed
Click the failed job to expand its log. Each step in the job is listed, and the failed step is automatically expanded to show you the error.
- If the build job failed at Run tests: your test file has a syntax error or import issue. Check that app.test.js matches the code from the previous step exactly.
- If the deploy job failed: verify that your GitHub secret AZURE_WEBAPP_PUBLISH_PROFILE contains the full XML contents of the publish profile file (not just the filename).
- If the deploy failed with an app name error: check that the AZURE_WEBAPP_NAME value in deploy.yml matches your actual App Service name in Azure Portal exactly.
After fixing the issue, commit and push the change. The pipeline will re-run automatically.
Verify the live deployment
Your app is now deployed to Azure App Service and accessible on the public internet. Let's confirm it's responding, then test the full CI/CD cycle by pushing a change.
- Return to your App Service in Azure Portal.
- On the Overview page, find the Default domain value (it looks like your-app-name.azurewebsites.net).
- Click the URL to open your live app in a new browser tab.
You should see a JSON response with "status": "healthy" and a message field confirming the pipeline is working.
Seeing a default Azure page or an error?
- If you see a default Azure placeholder page, the deploy job may still be in progress. Wait for the workflow to complete in the Actions tab, then refresh.
- If you see a 503 error, the Free F1 tier may still be starting up after the first deployment. Wait 30 seconds and refresh again.
- If the app never loads, confirm that server.js uses process.env.PORT for the port. Azure App Service sets this environment variable and your app must listen on it.
Help me troubleshoot my Azure App Service deployment
Now let's prove the full automated cycle works by making a change and watching it deploy.
- In your code editor, open app.js.
- Find the message value in the / route:
message: 'GitHub Actions CI/CD Pipeline is working!'
- Change the message text to anything you like (for example, add your name or change the wording).
- Save app.js.
- Commit and push the change by running these commands:
git add app.js
git commit -m "Update response message"
git push
- Switch back to the Actions tab in your GitHub repository.
- Watch the new workflow run appear and complete with green checkmarks on both jobs.
- Refresh your App Service URL in the browser.
You should see your updated message in the JSON response. This confirms the full CI/CD cycle is working. Every push to main automatically builds, tests, and deploys your app.
Why does it take a minute or two?
The pipeline spins up a fresh virtual machine, installs Node.js, runs your tests, and uploads an artifact before the deploy job even starts. This entire process runs on GitHub Actions infrastructure at no cost to you (public repos get unlimited free minutes).
On the Free F1 tier, the app may also need a few seconds to wake up after deployment because it can sleep when idle. This is normal for the Free tier.
Live site not showing the updated message?
- Make sure the new workflow run completed successfully (green checkmarks on both jobs) before refreshing your browser.
- Try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to bypass any cached response.
- If you updated the message value but also changed the test expectations in app.test.js, the tests may fail and block deployment. Check the Actions tab for errors.
Why isn't my updated code showing on the live Azure site?
Your pipeline is fully operational. Every push to main now triggers an automatic build, test, and deploy cycle. Next up, you will take this further with a staging environment in the Secret Mission.
Secret mission
Add a Staging Environment with PR Trigger
What if you could preview every change before it reaches production? In this secret mission, you'll add a staging environment that deploys automatically when you open a pull request. Only merged code reaches your live app.
Clean Up Your Resources
Clean Up Your Resources
This project uses Azure App Service on the Free F1 tier, so there are no ongoing costs while your apps are running. Your GitHub repository is also free for public repos. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Azure Resource Group (contains both production and staging App Services on Free F1 tier).
- GitHub repository (public, free).
- GitHub repository secrets (AZURE_WEBAPP_PUBLISH_PROFILE and AZURE_WEBAPP_PUBLISH_PROFILE_STAGING).
- Local project files (github-actions-azure-demo folder on your Desktop).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your pipeline.
- Your apps stay live on the Free F1 tier at no cost.
- The Free tier has CPU quotas, so your app may sleep after periods of inactivity.
- Your GitHub Actions workflows will continue to run on every push or PR to main.
Pause - I'll come back to this later
The Azure App Service Free F1 tier does not have a pause option. Your app either runs or is deleted. If you want to stop the pipeline from triggering, you can disable the workflow in GitHub.
- Go to your repository on GitHub.
- Click the Actions tab.
- Select the Build and Deploy to Azure workflow from the left sidebar.
- Click the ... menu (three dots) and select Disable workflow.
- Your Azure apps remain live but deployments will no longer trigger automatically.
- To resume later, return to the same menu and select Enable workflow.
Delete - I don't want to use this again
Remove all project resources and start fresh. This ensures nothing is left running.
Delete Azure resources:
- Go to portal.azure.com and search for your Resource Group name in the search bar.
- Select your Resource Group from the results.
- Click Delete resource group in the toolbar.
- Type the Resource Group name to confirm, then click Delete.
- This removes both the production and staging App Services in one action.
Delete the GitHub repository:
- Go to your repository on GitHub.
- Click Settings.
- Scroll down to the Danger Zone section.
- Click Delete this repository.
- Click I want to delete this repository, then click I have read and understand these effects.
- Type your repository name to confirm, then click Delete this repository.
Delete local project files:
- Delete your local project folder by running this command:
cd ~/Desktop
rm -rf github-actions-azure-demo
On Windows?
Open PowerShell and run the following instead:
cd ~\Desktop
Remove-Item -Recurse -Force github-actions-azure-demo
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-grade CI/CD pipeline that automatically builds, tests, and deploys your code every time you push to GitHub.
You've learned how to:
- Create a complete GitHub Actions workflow with separate build and deploy jobs that run automated tests and deploy on every push to main.
- Deploy a Node.js Express app to Azure App Service using publish profile authentication stored securely in GitHub Secrets.
- Verify continuous deployment by pushing code changes and watching them go live automatically within minutes.
- Secret Mission: Added a staging environment with PR-based deployments so code is validated before reaching production.
Ready to quiz yourself?