30 Automating the Pipeline with Continuous Integration
Build a real CI pipeline that validates your application build and runtime behavior.
In this lab, you will extend the work from the previous sessions and build a real Continuous Integration pipeline for your application.
Unlike the CI preview you may have done earlier, this lab focuses on trust: checking that your application installs, builds, starts, and answers a health request every time you push code. You will also identify what these checks leave untested.
Debrief and context
Before starting, take a moment to reflect:
- You built a real Node.js application
- You containerized it with Docker
- You orchestrated services with Docker Compose
- You may already have experimented with a minimal CI job
This lab connects all of that work into a single automated validation pipeline.
Verify your starting point
Continue with your own GitHub fork and the application you completed in Session 2. Keep the application in labs/20-containerized-node-app; do not move it to the repository root.
Ensure your repository contains:
- A working Node.js application
- A valid Dockerfile
- A working Docker Compose setup
- All recent changes pushed to GitHub
Return to the repository root, even if your terminal is still inside the Session 2 lab:
cd "$(git rev-parse --show-toplevel)"git statusgit branch --show-currentgit remote -vls labs/20-containerized-node-appConfirm that origin points to your fork. Commit your completed Session 2 work before continuing. Stay on the branch containing that work; do not switch to an older preview branch that lacks your application changes.
If your branch tracks a remote branch, pull its latest changes:
git pull --ff-onlyIf Git reports that the branch has no upstream, publish the branch and establish tracking:
git push -u origin HEADIf you completed lesson 16, find your existing CI Preview workflow. You will expand that workflow rather than keep a second preview running alongside it.
Introduce the CI workflow
You will use GitHub Actions to define your pipeline as code.
Create the workflow directory if it does not already exist:
mkdir -p .github/workflowsCreate or edit .github/workflows/ci.yml, reusing the filename from lesson 16. If you previously named it ci.yaml, rename that file to ci.yml rather than creating a duplicate.
The workflow must be in .github/workflows at the repository root, not inside the lab directory. GitHub Actions starts from its own checkout; your local terminal directory does not set the runner’s directory.
Define pipeline triggers
Configure the workflow so it runs on both pushes and pull requests.
Start with the following content:
name: CI
on: push: pull_request:This ensures validation runs whenever code is proposed or updated.
Define the build job
Define a job that runs on a Linux runner:
jobs: build: runs-on: ubuntu-latestAll validation steps will live inside this job. Indent steps: under build, at the same level as runs-on. Each step belongs inside that list.
Use the complete workflow below to check the nesting after assembling the pieces.
Check out the repository
Add a step to fetch your repository contents:
steps: - name: Check out repository uses: actions/checkout@v7This is equivalent to cloning the repository locally.
Run fast checks
Before building containers, install dependencies reproducibly and check JavaScript syntax.
Append these steps to the job’s steps list:
- name: Set up Node.js uses: actions/setup-node@v7 with: node-version: "24"
- name: Install dependencies working-directory: labs/20-containerized-node-app/app run: npm ci
- name: Check JavaScript syntax working-directory: labs/20-containerized-node-app/app run: | node --check app.js node --check server.jsnpm ci installs the versions recorded in the committed package-lock.json and fails if its dependency declarations disagree with package.json.
node --check catches syntax errors. It does not execute the application, resolve its imports, or verify its behavior.
The scaffold’s test script is a placeholder that prints “Error: no test specified” and exits with status 1. npm test --if-present would still run it because the script exists. Leave it out of the baseline pipeline; replace it with real tests if you choose the testing extension.
Build the Docker image
Add a step to build the application image:
- name: Build Docker image working-directory: labs/20-containerized-node-app run: docker build -t quote-app -f docker/Dockerfile .The working directory makes . the lab directory. This is the build context expected by the Dockerfile’s COPY app/ instructions. A successful build produces an image; it does not prove that the image starts.
Run a container smoke test
A successful build is not enough. Start the built image and check that /health responds successfully with JSON containing "ok": true.
Append these steps:
- name: Run container smoke test run: | docker run -d --name quote-app -p 3000:3000 quote-app for attempt in $(seq 1 30); do if curl --fail --silent --show-error \ --connect-timeout 1 --max-time 2 \ http://localhost:3000/health -o /tmp/quote-app-health.json \ && node -e 'const assert = require("node:assert/strict"); const fs = require("node:fs"); assert.equal(JSON.parse(fs.readFileSync("/tmp/quote-app-health.json", "utf8")).ok, true);'; then echo "Health check passed" exit 0 fi sleep 1 done echo "Health check failed" exit 1
- name: Show container diagnostics if: failure() run: | docker ps -a docker inspect quote-app || true docker logs quote-app || true
- name: Stop container if: always() run: docker rm -f quote-app || trueEach HTTP request has a timeout. The loop allows up to 30 attempts with a one-second pause between attempts; this is not an exact 30-second deadline. Add a five-minute job timeout in the complete workflow to bound the whole job.
Diagnostics run when a previous step fails, and cleanup runs even after failure.
The application listens on 0.0.0.0:3000, and the Dockerfile starts it with npm start. This baseline runs the image without PostgreSQL. The /health route does not query the database, so it can pass while the quote page or quote insertion fails.
Compare the complete workflow
Your repository-root .github/workflows/ci.yml should contain:
name: CI
on: push: pull_request:
permissions: contents: read
jobs: build: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Check out repository uses: actions/checkout@v7
- name: Set up Node.js uses: actions/setup-node@v7 with: node-version: "24"
- name: Install dependencies working-directory: labs/20-containerized-node-app/app run: npm ci
- name: Check JavaScript syntax working-directory: labs/20-containerized-node-app/app run: | node --check app.js node --check server.js
- name: Build Docker image working-directory: labs/20-containerized-node-app run: docker build -t quote-app -f docker/Dockerfile .
- name: Run container smoke test run: | docker run -d --name quote-app -p 3000:3000 quote-app for attempt in $(seq 1 30); do if curl --fail --silent --show-error \ --connect-timeout 1 --max-time 2 \ http://localhost:3000/health -o /tmp/quote-app-health.json \ && node -e 'const assert = require("node:assert/strict"); const fs = require("node:fs"); assert.equal(JSON.parse(fs.readFileSync("/tmp/quote-app-health.json", "utf8")).ok, true);'; then echo "Health check passed" exit 0 fi sleep 1 done echo "Health check failed" exit 1
- name: Show container diagnostics if: failure() run: | docker ps -a docker inspect quote-app || true docker logs quote-app || true
- name: Stop container if: always() run: docker rm -f quote-app || trueObserve the pipeline run
Commit and push your changes:
git add .github/workflows/ci.ymlgit commit -m "Add full CI pipeline"git pushOpen the Actions tab in GitHub and observe:
- Each pipeline step executing
- Logs for dependency installation
- Docker build output
- Container startup and health check
Before deliberately breaking anything, obtain a green baseline run. Diagnose unexpected failures first. The placeholder test script is not the intentional failure exercise.
Open a pull request from your working branch to your fork’s default branch to verify the pull_request trigger as well. If you are already working on the default branch, create and push a new branch with a small change for this check. Keep the pull request open for review; merging is not required.
Break the pipeline intentionally
This step is required for grading.
You must intentionally introduce a failure, observe a failing CI run on GitHub, then fix the issue and push a successful run.
Keep the failed run URL. During the final review, show that run and identify the failing step and the log message that explains the cause.
Examples:
- Change the health check URL to a non-existent path
- Introduce a JavaScript syntax error and observe the syntax check fail
- Introduce a syntax error in the Dockerfile
- Break the application startup
Commit and push, then observe:
- The pipeline failing
- Clear error logs
- Automatic detection of broken changes
Fix the issue and push again. Keep the recovery run URL and the fixing commit SHA.
Record the failed step, the cause, and why your change fixes it in a short note in the repository. Do not remove or bypass the failing check to get green. Keep both runs available as grading evidence.
Understand what CI gives you
A green baseline run means that, for the checked commit on this runner:
- Dependencies installed from the lockfile
- The two JavaScript files passed syntax checks
- The Docker image built
- The container answered
/healthwith the expected JSON value
It does not prove that database connections, quote retrieval, insertion, persistence, security, performance, or production deployment work. Explain at least one failure that could still occur with green CI.
CI provides a signal. It becomes a merge gate only when branch protection or repository rules require its checks to pass. This lab does not configure those rules.
Extension lanes
Complete at least one lane with working changes and verification evidence. An explanation alone, a smaller retry count, or extra logging does not satisfy the extension requirement.
Start an extension as soon as your baseline and deliberate failure/recovery exercise work. Continue improving it while waiting for your oral review.
Lane A: Validate database-backed behavior
- Extend CI to start PostgreSQL and the built application image, using the Session 2 database initialization script and
DATABASE_URLconfiguration. - Wait for database readiness and application startup with bounded checks.
- Verify that
/renders a seeded quote, submit a distinct quote to/quotes, then verify that a subsequent request returns it. Assert on the quote text; an HTTP 200 alone is insufficient. - Make initialization or a database query fail, show CI detecting it, then fix it and obtain green.
- Always clean up services and print application/database logs on failure.
Use the image’s production startup command. The local Compose development override (npm run dev and source mounts) can hide the image’s packaged source; use a dedicated CI Compose configuration or adapt it to run the built image without source mounts.
Acceptance: show the workflow changes, a green database-backed run, and a failed/recovered run proving that the new assertions detect a database or quote-operation problem.
Lane B: Add automated route tests
- Create tests inside
labs/20-containerized-node-app/app/testsusing Node’s built-in test runner and Fastify’sinjectmethod. - Import the named async export with
import { buildApp } from '../app.js', awaitbuildApp(), and close each app in afinallyblock. - Test
/healthwith assertions on its HTTP status and JSON body. Add a second case, such as an unknown route returning 404 or a database-backed quote route. - Replace the placeholder
testscript withnode --testand runnpm testlocally from the app directory. - Add an explicit
npm teststep to CI after dependency installation, using the same app working directory. - Change application behavior so an assertion fails, show the failed CI test, then repair it.
Acceptance: show both test cases, the real test script, the CI test step, and failed/recovered runs. Explain what in-memory route tests leave untested about the Docker image and deployment.
Lane C: Separate fast checks from container validation
- Create a fast-checks job for dependency installation and syntax checks (and tests if you add them).
- Create a second job for Docker build and runtime validation, with
needsreferencing the fast-checks job. - Give each job its own checkout and required setup. Jobs do not share a workspace, installed dependencies, or locally built images automatically.
- Introduce a syntax error and show the fast-checks job failing while the container job is skipped.
- Repair the error and show both jobs passing, including a pull-request run.
Acceptance: show the dependency between jobs, failed/recovered runs, and a pull-request run. Explain why the container job must not report success when the prerequisite checks fail.
Prepare your grading evidence
Before the oral review, have these items ready:
- Your fork, working branch, and workflow file
- A green baseline run and a pull-request run
- The deliberate failed run, its diagnostic log, the fixing commit, and the recovered green run
- Your extension changes and verification evidence
- Your short diagnosis note and an explanation of what green CI does and does not prove
The review focuses on four broad areas: a working pipeline, failure diagnosis and recovery, a meaningful extension, and your understanding. Keep the relevant GitHub tabs open so you can show evidence promptly.
Wrap-up
You have built a complete CI pipeline that:
- Builds your application
- Verifies dependencies
- Confirms the container starts and answers the health request
- Makes failures visible with diagnostic evidence
In larger systems, this stage is often extended with full end-to-end tests using browser-based tools such as Playwright.
This is intentionally out of scope here, but your current pipeline shows where such tests would fit in a real production workflow.
This lab completes the graded work for the DevOps course.
In the next course, you will take these same ideas further with orchestration, virtualization, and infrastructure-level automation.