cat 0022-nodejs-cicd-step-by-step.md
Node.js CI/CD, Step by Step
From writing code locally to automatically testing, building, containerizing, and delivering it.
🧭 What Are We Building?
Writing code is only one part of building software. Imagine you have a Node.js application. Every time you make a change, someone needs to:
Write code
↓
Run tests
↓
Check linting
↓
Check formatting
↓
Build the application
↓
Create a Docker image
↓
Push the image
↓
Deploy the application
If humans have to remember every step, eventually something gets missed. Maybe someone forgets to run the tests. Maybe the TypeScript build is broken. Maybe the application works locally but fails inside Docker. Maybe a vulnerable dependency gets introduced.
This is the problem CI/CD helps solve. Instead of depending on memory, we build a system that automatically performs these checks for us. By the end of this tutorial, our workflow will look like this:
👨💻 Developer
│
│ git push
▼
GitHub
│
▼
⚙️ GitHub Actions
│
┌────────────┼────────────┐
▼ ▼ ▼
Format Lint Security
│ │ │
└────────────┼────────────┘
▼
Build
│
▼
Test
│
▼
🐳 Docker Build
│
▼
📦 Docker Registry
│
▼
🚀 Deployment
The goal isn't just to make this work. The goal is to understand why each stage exists.
🧠 1. First, What Exactly Is CI/CD?
Before touching GitHub Actions, Docker, or YAML, let's understand the idea.
CI - Continuous Integration
Continuous Integration means continuously integrating code changes and automatically verifying that those changes haven't broken the project.
For example:
git push origin maincould automatically trigger:
Install dependencies
↓
Check formatting
↓
Run linting
↓
Compile TypeScript
↓
Run tests
↓
Check dependencies
If something fails, we know immediately. Instead of discovering the problem after deployment, we discover it during development.
CD - Continuous Delivery / Deployment
Once the code passes CI, we can automatically prepare it for delivery.
For example:
Tests pass
↓
Build application
↓
Create Docker image
↓
Push image to registry
↓
Deploy
There are two commonly used meanings of CD.
Continuous Delivery
The application is automatically built and prepared for deployment.
A human may still approve the production release.
Continuous Deployment
The deployment itself happens automatically after the pipeline succeeds.
In this tutorial, we'll build the foundation for both.
🗺️ 2. Our Learning Roadmap
We'll build the project layer by layer.
┌─────────────────────────────────────┐
│ Node.js + TypeScript │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Automated Tests │
│ Jest + Supertest │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Code Quality │
│ ESLint + Prettier │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Git Hooks │
│ Husky + lint-staged │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Continuous Integration │
│ GitHub Actions │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Containerization │
│ Docker │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Container Registry │
│ Docker Hub │
└──────────────────┬──────────────────┘
↓
┌─────────────────────────────────────┐
│ Deployment │
└─────────────────────────────────────┘
Each layer solves a different problem.
🏗️ 3. Create the Project
Let's start from an empty directory.
mkdir node-cicd-demo
cd node-cicd-demo
npm init -yWe now have:
node-cicd-demo/
└── package.json
Install TypeScript and the tools we'll need for development:
npm install --save-dev \
typescript@^5.9.3 \
ts-node \
nodemon \
@types/nodeCreate the source directory:
mkdir srcOur project is currently very small. That's intentional. We're going to build the CI/CD system around this small application.
⚙️ 4. Configure TypeScript
Create:
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}What does this configuration do?
Our source code lives here:
src/
TypeScript compiles it into:
dist/
So the process is:
src/server.ts
│
│ TypeScript compiler
▼
dist/server.js
During development, we work with TypeScript. In production, Node.js runs the compiled JavaScript.
🌐 5. Build a Simple Express API
Install Express:
npm install expressAnd the TypeScript types:
npm install --save-dev @types/expressNow create:
src/app.ts
import express, { Application, NextFunction, Request, Response } from "express";
const app: Application = express();
app.use(express.json());
app.get("/health", (_req: Request, res: Response) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || "development",
});
});
app.get("/", (_req: Request, res: Response) => {
res.json({
message: "Node.js CI/CD Demo API",
});
});
app.get("/api/users", (req: Request, res: Response) => {
const users = [
{
id: 1,
name: "John Doe",
email: "john@example.com",
},
{
id: 2,
name: "Jane Smith",
email: "jane@example.com",
},
];
const limit = Number(req.query.limit);
if (Number.isInteger(limit) && limit > 0) {
return res.json({
users: users.slice(0, limit),
});
}
res.json({ users });
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
res.status(500).json({
error: "Internal server error",
});
});
export default app;💡 Why Separate app.ts and server.ts?
This small decision becomes important when we start testing.
We want:
app.ts
│
└── Configure Express
server.ts
│
└── Start HTTP server
Create:
src/server.ts
import app from "./app";
const PORT = Number(process.env.PORT) || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Now add the scripts:
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}Run the application:
npm run devVisit:
http://localhost:3000
You should see:
{
"message": "Node.js CI/CD Demo API"
}And:
http://localhost:3000/health
should return:
{
"status": "ok",
"timestamp": "...",
"environment": "development"
}🎉 We have our application. But we don't have CI/CD yet.
🧪 6. Add Automated Testing
Before we can automate quality checks, we need to define what "working" means. That's what tests do.
We'll use:
- Jest → test runner
- Supertest → HTTP API testing
Install them:
npm install --save-dev \
jest \
ts-jest \
@types/jest \
supertest \
@types/supertestCreate jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.d.ts",
"!src/**/__tests__/**",
"!src/server.ts",
],
coverageThreshold: {
global: {
functions: 70,
lines: 70,
statements: 70,
},
},
coverageReporters: ["text", "lcov", "html"],
};Also update tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true, // Add this line
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"types": ["jest", "node"] // Add this line
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}✍️ 7. Write Our First Tests
Create:
src/__tests__/app.test.ts
import request from "supertest";
import app from "../app";
describe("API", () => {
describe("GET /health", () => {
it("should return healthy status", async () => {
const response = await request(app).get("/health").expect(200);
expect(response.body).toMatchObject({
status: "ok",
});
expect(response.body).toHaveProperty("timestamp");
});
});
describe("GET /", () => {
it("should return welcome message", async () => {
const response = await request(app).get("/").expect(200);
expect(response.body).toEqual({
message: "Node.js CI/CD Demo API",
});
});
});
describe("GET /api/users", () => {
it("should return users", async () => {
const response = await request(app).get("/api/users").expect(200);
expect(response.body.users).toHaveLength(2);
});
it("should respect the limit query parameter", async () => {
const response = await request(app).get("/api/users?limit=1").expect(200);
expect(response.body.users).toHaveLength(1);
});
});
describe("404", () => {
it("should return 404 for unknown routes", async () => {
await request(app).get("/does-not-exist").expect(404);
});
});
});Add the scripts:
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}Run:
npm testAnd:
npm run test:coverageNow we have our first quality gate.
🧠 Quality Gate
A quality gate is simply a condition that must pass before the code is allowed to move to the next stage.
For example:
Tests pass → Continue
Tests fail → Stop
🧹 8. Add ESLint
Tests answer:
Does the application behave correctly?
ESLint answers:
Does the code follow our coding rules?
Install ESLint:
npm install --save-dev eslint @eslint/js typescript typescript-eslint globalsCreate:
eslint.config.mjs
// @ts-check
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import globals from "globals";
export default defineConfig([
// Standalone ignores object = Global ignore rules
{
ignores: ["dist/**", "coverage/**", "node_modules/**"],
},
{
files: ["**/*.{js,ts}"],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "warn",
},
},
]);Add:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}Run:
npm run lintIf ESLint finds a problem, the command exits with a non-zero status. That's important.
CI systems understand this:
0 → ✅ Success
non-zero → ❌ Failure
That simple mechanism is the foundation of automated pipelines.
🎨 9. Add Prettier
ESLint and Prettier solve different problems.
ESLint
│
└── Code quality + rules
Prettier
│
└── Code formatting
Install Prettier:
npm install --save-dev prettierCreate:
.prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}Create:
.prettierignore
node_modules
dist
coverage
.git
*.log
.env
Add:
{
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}Now:
npm run formatformats the project.
While:
npm run format:checkonly checks whether everything is formatted correctly. The second command is perfect for CI.
🌿 10. Initialize Git & Repository Setup
Before configuring local automation hooks, we must establish our project as a Git repository.
📁 10.1 Create a .gitignore File
Create a .gitignore file in your root folder to ensure dependencies, secrets, and build outputs are not tracked:
node_modules/
dist/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
.vscode/
🚀 10.2 Initialize the Git Repository
Run the following commands in your terminal to initialize Git and create your first commit:
git init # Initialize git
git branch -M main # Rename default branch to 'main'
# Stage and commit your files
git add .
git commit -m "chore: initial project setup"🌐 10.3 Connect to GitHub
Create a blank repository on GitHub, then link it and push your code:
# Connect local repo to remote GitHub repo
git remote add origin https://github.com/YOUR_USERNAME/node-ts-cicd-demo.git# Push code to main branch
git push -u origin main🪝 11. Add Git Hooks (Husky & lint-staged)
We can run tests, linting, formatting, and builds manually. But waiting for GitHub Actions to catch simple syntax errors wastes time.
By using Git hooks, we catch formatting issues and syntax mistakes locally, before the commit is even allowed to be created.
⚠️ Prerequisite: This step requires the Git repository to be initialized first (completed in 10).
📦 11.1 Installation
Install Husky (to manage hooks) and lint-staged (to target only modified files) as development dependencies:
npm install --save-dev husky lint-staged⚙️ 11.2 Configuration
Step A: Initialize Husky
Run the initialization command to map Husky directly into your .git folder setup:
npx husky initThis command automatically creates a .husky/ directory in your root folder with a pre-commit script file inside.
Step B: Configure the Pre-Commit Hook
Open .husky/pre-commit in your code editor and change its contents to execute lint-staged:
# .husky/pre-commit
npx lint-stagedStep C: Configure lint-staged Rules
Open your package.json file and append the lint-staged configuration block at the root level:
{
"name": "node-cicd-demo",
"version": "1.0.0",
"scripts": { ... },
"dependencies": { ... },
"lint-staged": {
"*.{ts,js}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}🧪 11.3 Verify the Hook with a Test Commit
Now that Husky is initialized and linked to your Git configuration, you do not need to create dummy edits to test it.
Simply stage your newly modified configuration files (package.json, .husky/pre-commit, etc.) and create a standard commit. The hook will automatically trigger for the first time:
git add .
git commit -m "chore: setup husky and lint-staged automation"You will see your terminal instantly invoke lint-staged right before the commit finalizes, confirming your automation works perfectly on your actual project files.
🔄 How the Workflow Changes
Every time you execute git commit, the automated local validation kicks in:
[ Developer changes code ]
│
▼
git commit
│
▼
[ Husky Triggers ]
│
▼
[ lint-staged Filters ] ───► (Processes ONLY modified files)
│
▼
[ ESLint + Prettier Runs ]
│
┌──────────┴──────────┐
│ │
▼ ▼
[ Fixes Passed ] [ Errors Found ]
│ │
▼ ▼
Commit Saved Commit Blocked 🛑 (Fix errors and retry)
🧠 The Takeaway
Think of Git hooks as your first line of defense. They keep the codebase healthy, maintain a clean commit history, and prevent broken configurations from ever leaving your machine. However, because local hooks can be bypassed (--no-verify), our upcoming GitHub Actions CI pipeline remains the ultimate safety net.
⚙️ 12. Design the CI Pipeline Before Writing YAML
Before writing GitHub Actions configuration, let's decide what we want. Every pull request should go through:
Pull Request
│
▼
Formatting
│
▼
Lint
│
▼
Build
│
▼
Tests
│
▼
Security
│
▼
✅ Pass
If any stage fails:
❌ FAIL
│
▼
Stop pipeline
│
▼
Fix the code
This is the heart of CI.
🚀 13. Create GitHub Actions Workflow
Create:
.github/
└── workflows/
└── ci.yml
Add:
name: CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
env:
NODE_VERSION: 22
jobs:
quality:
name: Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run test:coveragePush the workflow:
git add .
git commit -m "ci: add GitHub Actions pipeline"
git pushNow go to:
GitHub → Actions
At this point, we could push the workflow to GitHub and wait for GitHub Actions to execute it. But there's a useful tool that lets us test much of this workflow locally first.
🧪 14. Test GitHub Actions Locally with act
Writing a GitHub Actions workflow and pushing it to GitHub just to see if it works is slow. Every iteration costs a commit, a push, and a wait for the runner to spin up. act solves this by running your workflows locally inside Docker containers.
actis a CLI tool that reads your.github/workflows/files and executes them on your machine using Docker. It emulates the GitHub Actions runner environment as closely as possible.
🛠️ 14.1 Install act
act is available for Linux, macOS, and Windows. Choose the method that fits your system.
# macOS
brew install act
# Linux
curl -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash -s -- -b /usr/local/bin
# Windows
choco install act-cli # or: scoop install actVerify:
act --version🐳 14.2 Make Sure Docker Is Running
docker psIf this fails, start Docker Desktop or the Docker daemon.
⚙️ 14.3 Create a .actrc File
Instead of typing the same flags every time, create a .actrc file in your project root:
# .actrc
-P ubuntu-latest=catthehacker/ubuntu:act-latest
--reuseWhy? Without it, every command looks like this:
act -j quality -P ubuntu-latest=catthehacker/ubuntu:act-latest --reuseWith .actrc, it's just:
act -j quality🚀 14.4 Run Your Workflow
From your project root:
actRuns all workflows on the push event. The first run downloads the runner image (a few minutes). Later runs are fast.
Run a single job
act -j qualitySee available jobs
act -lSimulate a pull request
act pull_request
🔧 14.5 Common Flags
| Flag | Description |
|---|---|
-l |
List workflows and jobs |
-j <job_id> |
Run a specific job |
-e <event> |
Trigger a specific event (e.g. pull_request) |
-s KEY=value |
Pass a secret |
--secret-file <path> |
Load secrets from a file |
-P ubuntu-latest=<image> |
Use a specific runner image |
--reuse |
Reuse containers between runs |
--dryrun |
Show what would run without running it |
📝 14.6 Example
act -j qualityOutput:
[CI/quality] ⭐ Run Main Checkout repository
[CI/quality] ✅ Success - Main Checkout repository
[CI/quality] ⭐ Run Main Setup Node.js
[CI/quality] ✅ Success - Main Setup Node.js
[CI/quality] ⭐ Run Main Install dependencies
[CI/quality] ✅ Success - Main Install dependencies
[CI/quality] ⭐ Run Main Lint
[CI/quality] ✅ Success - Main Lint
[CI/quality] ⭐ Run Main Test
[CI/quality] ✅ Success - Main Test
[CI/quality] 🏁 Job succeeded
If a step fails, act shows the error just like GitHub would.
🛡️ 15. Add Dependency Security Checks
Our application depends on many packages.
For example:
Express
Jest
Supertest
ESLint
TypeScript
...
Those packages may themselves depend on other packages. So our dependency tree can become large:
Your App
│
├── Express
│ ├── Package A
│ └── Package B
│
├── Jest
│ ├── Package C
│ └── Package D
│
└── ...
A vulnerability can exist deep inside that tree.
We can use:
npm auditAdd another CI job:
security:
name: Dependency Security
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=highPipeline gates now include:
Format → Lint → Build → Test → Security
🐳 16. Containerize the Application
Docker gives production a predictable runtime. Use a multi-stage build:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK \
--interval=30s \
--timeout=3s \
--start-period=5s \
--retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
CMD ["node", "dist/server.js"]Why multi-stage?
- Builder stage: TypeScript, source, dev dependencies.
- Production stage: compiled JS + production dependencies only.
This keeps the production image smaller and cleaner.
Create .dockerignore:
node_modules
dist
coverage
.git
.github
.env
.env.*
README.md
.vscode
*.log
Test locally:
docker build -t node-cicd-demo .
docker run --rm -p 3000:3000 node-cicd-demoCheck:
http://localhost:3000/health
If it returns { "status": "ok" }, the container works.
📦 17. Container Registry
Production servers can’t pull from your laptop. Images must be stored in a registry.
Common registries:
- Docker Hub
- GitHub Container Registry
- Amazon ECR
- Google Artifact Registry
- Azure Container Registry
Flow:
GitHub Actions → Docker Image → Registry → Production Server
For this tutorial, use Docker Hub. Create a Docker Hub repository:
yourusername/node-cicd-demo
In GitHub, add repository secrets:
Settings → Secrets and variables → Actions
Create:
DOCKER_USERNAME
DOCKER_PASSWORD
Never hardcode secrets in the workflow. Use:
password: ${{ secrets.DOCKER_PASSWORD }}🚀 18. Build and Push the Docker Image
Add a Docker job after quality and security pass.
docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs:
- quality
- security
if: |
github.event_name == 'push' &&
github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/node-cicd-demo:latest
${{ secrets.DOCKER_USERNAME }}/node-cicd-demo:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxTwo tags are useful:
latest— newest published image.<commit-sha>— exact version tied to a Git commit.
Use the SHA tag for traceability and safer deployments.
🔄 19. Complete CI/CD Flow
Developer
│ git push
▼
GitHub
│
▼
GitHub Actions
├── Format
├── Lint
├── Build
├── Test
└── Security
│
▼
Docker Build
│
▼
Docker Hub
│
▼
Deployment
Deploy manually with:
docker pull yourusername/node-cicd-demo:latest
docker run -d \
--name node-cicd-demo \
-p 3000:3000 \
yourusername/node-cicd-demo:latestThis is continuous delivery. To become continuous deployment, automate the server pull/run step via SSH, cloud platforms, Kubernetes, ECS, Cloud Run, etc.
🧠 20. The Three Layers
-
Local feedback
Husky, lint-staged, ESLint, Prettier, tests.
Catch problems before code leaves the machine.
-
Continuous Integration
GitHub Actions.
Verify the shared repository is healthy: format, lint, build, test, security.
-
Delivery & Deployment
Docker, registry, cloud/server.
Package, distribute, and run the application.
💥 21. Failure and Pull Requests
If any CI stage fails, the pipeline stops. Docker image is not published.
Example:
Install ✓ → Format ✓ → Lint ✓ → Build ✗
Result: broken version does not move forward.
For pull requests:
feature branch → PR → GitHub Actions → PASS/FAIL → Review/Fix
CI gives the team automated feedback before merging.
⚠️ 22. Common Mistakes
- Use
npm ci, notnpm install, in CI. - Don’t run
npm run devin production. Run compiled JS:node dist/server.js. - Don’t ship dev dependencies. Use
npm ci --omit=dev. - Never commit secrets. Use GitHub Secrets or a secret manager.
- Don’t rely only on
latest. Use immutable tags like<commit-sha>. - Don’t assume Git hooks are enough. They can be bypassed with
-no-verify. - Local hooks give fast feedback; CI is the shared safety net.
🔁 23. Everyday Workflow
git checkout -b feature/new-featureMake changes. Run locally:
npm run lint
npm run format:check
npm run build
npm testCommit:
git add .
git commit -m "feat: add new feature"Husky runs automatically.
Push:
git push origin feature/new-featureOpen a PR. GitHub Actions runs:
Format → Lint → Build → Tests → Security
After merge to main:
CI → Docker Build → Docker Image → Docker Hub → Deploy
🧩 24. What Each Stage Answers
| Stage | Question |
|---|---|
| 🎨 Prettier | Is the code consistently formatted? |
| 🧹 ESLint | Does the code follow our rules? |
| 🧪 Tests | Does the application behave correctly? |
| 🔨 Build | Can the application compile? |
| 🛡️ Security | Do dependencies contain known vulnerabilities? |
| 🐳 Docker | Can we package the application consistently? |
| 📦 Registry | Can we distribute the image? |
| 🚀 Deployment | Can we run this version in an environment? |
🎯 Final Mental Model
CI/CD is not about memorizing YAML. It is about creating a reliable path from code to production.
Code
↓
Validate
↓
Test
↓
Build
↓
Package
↓
Release
↓
Deploy
↓
Observe
CI/CD is not about automating commands. It’s about creating a reliable path from code to production.