# CI/CD Pipeline Part 1: Automated Builds and Security Scanning with GitHub Actions

> **Series Navigation:**
> 
> * **Part 0**: [Introduction - Why This Setup?](https://devnotes.kamalthennakoon.com/from-local-to-live-your-strapi-deployment-roadmap)
>     
> * **Part 1**: [Containerizing Strapi v5](https://devnotes.kamalthennakoon.com/containerizing-strapi-v5-for-production-the-right-way)
>     
> * **Part 2**: [Deploying to DigitalOcean](https://devnotes.kamalthennakoon.com/deploying-strapi-v5-to-digitalocean-docker-compose-in-action)
>     
> * **Part 3**: [Production Web Server Setup](https://devnotes.kamalthennakoon.com/setting-up-nginx-and-ssl-making-your-strapi-backend-production-ready)
>     
> * **Part 4**: [Automated Database Backups](https://devnotes.kamalthennakoon.com/automated-database-backups-for-strapi-v5-aws-s3-setup)
>     
> * **Part 5a**: CI Pipeline with GitHub Actions *(You are here)*
>     
> * **Part 5b**: CD Pipeline and Deployment Automation *(Coming next week)*
>     
> 
> **New to the series?** Each article works standalone, but if you want to follow along with actual deployments, start with Parts 1-4.

---

Alright, we've built a solid infrastructure - containerized Strapi, DigitalOcean deployment, Nginx with SSL, and automated backups. But here's the problem: every time you want to deploy new code, you're manually building Docker images, pushing them to GitHub Container Registry, SSHing into your server, pulling the new image, and restarting containers.

*That works fine when you're deploying once a week. But as your project grows? You'll be deploying daily, maybe multiple times a day. Manual deployments become a bottleneck fast.*

This is where CI/CD comes in. In this article (Part 5a), we're building the **CI (Continuous Integration)** part - automated validation and build verification every time you push code. We'll create a GitHub Actions workflow that:

* Automatically validates your code when you push to any feature or fix branch
    
* Runs security scans to catch vulnerabilities early
    
* Checks code quality (ESLint if you have it configured)
    
* Verifies your Docker image builds successfully
    
* Gives you that green checkmark (or red X) on every commit
    

In Part 5b, we'll add the **CD (Continuous Deployment)** part - actually deploying those validated builds to your DigitalOcean server with proper approval gates and rollback capabilities.

**Why split this into two articles?** I originally planned to cover CI/CD in one go, but that would've been a 30-40 minute read. Nobody wants to sit through that. Breaking it into two parts makes it digestible and lets you test the CI pipeline before adding deployment complexity.

Let's build this thing.

---

## **What Is CI/CD Anyway?**

If you're new to these terms, here's the quick version:

**CI (Continuous Integration):**

* Automatically validate and test your code when you push changes
    
* Catch bugs early, before they reach production
    
* Make sure your code actually compiles and passes basic checks
    
* Create a culture where "broken builds" get fixed immediately
    

**CD (Continuous Deployment/Delivery):**

* Automatically deploy those validated builds to your servers
    
* Add approval gates so you control when things go live
    
* Provide rollback mechanisms when things break
    
* Make deployments boring and predictable
    

**The philosophy:**

* Small, frequent changes are safer than big, scary deployments
    
* Automate the boring, error-prone stuff (validating, testing, deploying)
    
* Spend your time building features, not babysitting deployments
    

*For our staging environment, we're building a system that's professional without being overkill. You won't need a full-time DevOps team to maintain this.*

---

## **Understanding the Green Checkmark (For Beginners)**

If you've used GitHub before, you've probably seen green checkmarks and red X marks next to commits. Here's what they actually mean:

**The Green Checkmark (✅):**

* Your code passed all automated checks
    
* It compiles, builds, and doesn't have obvious issues
    
* Safe to review and potentially merge
    

**The Red X (❌):**

* Something failed - maybe security issues, build errors, or test failures
    
* Don't merge this yet - fix the problems first
    
* Click on it to see what went wrong
    

**Why this matters:**

Before CI, you'd push code and only discover it broke things when someone tried to deploy or run it. With CI, you know immediately if your changes broke something. The faster you catch issues, the easier they are to fix.

**In practice:**

* Push a feature branch → See green checkmark → Create pull request with confidence
    
* Push a feature branch → See red X → Fix issues before anyone reviews
    
* Review a pull request → See green checkmark → Know the code at least compiles
    

This workflow makes code reviews way more productive because you're discussing the actual logic, not debugging basic build failures.

---

## **What We're Building in Part 5a**

By the end of this article, you'll have a CI pipeline that runs on every push to:

* `main` branch
    
* `dev` branch
    
* `feature/**` branches
    
* `fix/**` branches
    
* Any pull requests targeting `main` or `dev`
    

**What the pipeline does:**

**1\. Code Quality Check:**

* Runs ESLint if you have it configured
    
* Skips gracefully if you don't (workflow works for everyone)
    
* Helps catch common code issues early
    

**2\. Security Audit:**

* Runs `npm audit` to check for vulnerable dependencies
    
* Reports high and moderate severity issues
    
* Doesn't fail the build (just warns you to fix them)
    

**3\. Build Verification:**

* Attempts to build your Docker image
    
* Uses caching to make builds faster (2-5 minutes)
    
* Proves your code can compile into a deployable image
    
* **Does NOT push to GHCR** (we'll explain why in a moment)
    

**4\. Clear Summary:**

* Shows all check results in one place
    
* Makes it obvious what passed or failed
    
* Provides context (branch, commit) for debugging
    

**The Result:** Push any code → Wait 5-7 minutes → See green checkmark or red X → Know if your code is deployment-ready

---

## **Why We're NOT Pushing to GHCR in CI**

You might wonder: "Why build the image but not push it to GitHub Container Registry?"

Here's the problem with pushing every build:

**The Issue:**

* You push 20 feature branches while working on different ideas
    
* Each push creates a new image in GHCR
    
* GHCR has storage limits (500MB free tier, 2GB on Pro)
    
* Your registry fills up with images you'll never use
    
* Cleaning them up manually is a pain
    

**Our Approach:**

* CI just verifies the image *can* build successfully
    
* The actual image gets discarded after verification
    
* Only deployments (Part 5b) push to GHCR
    
* This keeps your registry clean and organized
    

**If you really want every build in GHCR:**

You can modify the workflow to push on specific branches. We'll mention how to do this later. But for most developers working on multiple features, the build-only approach is cleaner.

*The CD workflow in Part 5b will build and push to GHCR only when you're actually deploying to staging.*

---

## **Prerequisites**

Before we start, make sure you have:

* **Parts 1-4 completed** (containerized Strapi with working deployment)
    
* **Code in a GitHub repository** (public or private)
    
* **Dockerfile.stg** in your project root (from Part 1)
    
* **Dev branch** created (or working directly on main)
    
* About 45-60 minutes
    

*If you're working on multiple features, I recommend creating a* `dev` branch for active development and keeping `main` for production-ready code.

---

## **Step 1: Understanding GitHub Actions Basics**

Before we write the workflow, let's quickly cover how GitHub Actions actually works.

### **The Basics**

**Workflow:**

* A YAML file in `.github/workflows/` directory
    
* Defines when to run (on push, pull request, schedule, etc.)
    
* Contains one or more jobs
    

**Job:**

* A collection of steps that run together
    
* Runs on a virtual machine (runner)
    
* Can depend on other jobs
    

**Step:**

* Individual task within a job
    
* Can run shell commands or use pre-built actions
    
* Has access to your repository code
    

**Runner:**

* Virtual machine that executes your workflow
    
* GitHub provides free runners (Ubuntu, Windows, macOS)
    
* For public repos: unlimited minutes
    
* For private repos: 2,000 free minutes/month (plenty for us)
    

### **Why GitHub Actions?**

You might wonder why we're using GitHub Actions instead of Jenkins, CircleCI, or GitLab CI:

**Reasons it works for us:**

* Built into GitHub (no external service to set up)
    
* Free for public repos, generous free tier for private
    
* Great Docker support out of the box
    
* Easy to test and debug
    
* Popular (lots of community actions and examples)
    

For a staging environment? GitHub Actions is perfect.

---

## **Step 2: Create the CI Workflow**

Now let's create the GitHub Actions workflow file.

### **Create Workflow Directory**

```bash
# In your project root
mkdir -p .github/workflows
```

### **Create the CI Workflow File**

```bash
# Create the workflow file
nano .github/workflows/ci.yml
```

Paste this complete workflow:

```yaml
name: 🔄 Continuous Integration

# ═══════════════════════════════════════════════════════════════════
# Triggers: Runs automatically on every push and PR
# ═══════════════════════════════════════════════════════════════════
on:
  push:
    branches:
      - main
      - dev
      - 'feature/**'
      - 'fix/**'
  pull_request:
    branches:
      - main
      - dev

# ═══════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════
env:
  NODE_VERSION: '20'

permissions:
  contents: read

jobs:
  # ═══════════════════════════════════════════════════════════════════
  # JOB 1: Code Quality & Linting
  # ═══════════════════════════════════════════════════════════════════
  lint:
    name: 🔍 Code Quality
    runs-on: ubuntu-latest
    
    steps:
      - name: 📥 Checkout code
        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 ESLint (if configured)
        run: |
          if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
            echo "ESLint config found, running linter..."
            npm run lint || echo "⚠️  No lint script found in package.json"
          else
            echo "ℹ️  No ESLint config found, skipping..."
          fi
        continue-on-error: true

      - name: 📊 Lint summary
        run: echo "✅ Code quality check completed" >> $GITHUB_STEP_SUMMARY

  # ═══════════════════════════════════════════════════════════════════
  # JOB 2: Security Audit
  # ═══════════════════════════════════════════════════════════════════
  security:
    name: 🔒 Security Audit
    runs-on: ubuntu-latest
    
    steps:
      - name: 📥 Checkout code
        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 security audit
        run: |
          echo "## 🔒 Security Audit Results" >> $GITHUB_STEP_SUMMARY
          if npm audit --audit-level=moderate; then
            echo "✅ No security vulnerabilities found!" >> $GITHUB_STEP_SUMMARY
          else
            echo "⚠️  Security vulnerabilities detected - check details above" >> $GITHUB_STEP_SUMMARY
          fi
        continue-on-error: true

      - name: 📊 Generate detailed report
        if: always()
        run: |
          echo "### Vulnerability Details:" >> $GITHUB_STEP_SUMMARY
          npm audit --json > audit-report.json || true
          
          # Count vulnerabilities
          HIGH=$(cat audit-report.json | grep -o '"severity":"high"' | wc -l || echo "0")
          MODERATE=$(cat audit-report.json | grep -o '"severity":"moderate"' | wc -l || echo "0")
          
          echo "- **High**: $HIGH" >> $GITHUB_STEP_SUMMARY
          echo "- **Moderate**: $MODERATE" >> $GITHUB_STEP_SUMMARY

  # ═══════════════════════════════════════════════════════════════════
  # JOB 3: Build Verification
  # ═══════════════════════════════════════════════════════════════════
  build:
    name: 🏗️ Build Verification
    runs-on: ubuntu-latest
    needs: [lint, security]
    
    steps:
      - name: 📥 Checkout code
        uses: actions/checkout@v4

      - name: 🛠️ Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: 🐳 Build Docker image (test only)
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile.stg
          push: false
          tags: test-build:latest
          platforms: linux/amd64
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: ✅ Build successful
        run: |
          echo "## ✅ Build Verification Passed" >> $GITHUB_STEP_SUMMARY
          echo "Docker image builds successfully!" >> $GITHUB_STEP_SUMMARY

  # ═══════════════════════════════════════════════════════════════════
  # JOB 4: Summary (Final Status)
  # ═══════════════════════════════════════════════════════════════════
  ci-success:
    name: ✅ CI Complete
    runs-on: ubuntu-latest
    needs: [lint, security, build]
    if: success()
    
    steps:
      - name: 🎉 All checks passed
        run: |
          echo "## ✅ Continuous Integration Passed!" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "All validation checks completed successfully:" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Code quality check" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Security audit" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Build verification" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**This code is safe to deploy!** 🚀" >> $GITHUB_STEP_SUMMARY

  ci-failure:
    name: ❌ CI Failed
    runs-on: ubuntu-latest
    needs: [lint, security, build]
    if: failure()
    
    steps:
      - name: ❌ Some checks failed
        run: |
          echo "## ❌ Continuous Integration Failed" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "One or more validation checks failed." >> $GITHUB_STEP_SUMMARY
          echo "Please review the failed jobs above and fix issues before deploying." >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
          exit 1
```

Save and exit.

### **Understanding the Workflow Structure**

Let's break down what this workflow actually does:

#### **Workflow Triggers**

```yaml
on:
  push:
    branches:
      - main
      - dev
      - 'feature/**'
      - 'fix/**'
  pull_request:
    branches:
      - main
      - dev
```

**What this means:**

* Runs on every push to `main`, `dev`, `feature/*`, and `fix/*` branches
    
* Runs on pull requests targeting `main` or `dev`
    
* The `'feature/**'` pattern matches `feature/user-auth`, `feature/payment-system`, etc.
    
* The `'fix/**'` pattern matches `fix/login-bug`, `fix/memory-leak`, etc.
    

**Adding more branch patterns:**

If your team uses other naming conventions (like `hotfix/**`, `bugfix/**`, `chore/**`), just add them to the list:

```yaml
branches:
  - main
  - dev
  - 'feature/**'
  - 'fix/**'
  - 'hotfix/**'    # Add this if you use hotfix branches
  - 'bugfix/**'    # Add this if you use bugfix branches
```

The workflow is flexible, adjust it to match your team's branching strategy.

---

### **Job 1: Code Quality (ESLint)**

```yaml
lint:
  name: 🔍 Code Quality
  runs-on: ubuntu-latest
```

This job checks your code quality using ESLint.

**The smart part:**

```bash
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
  echo "ESLint config found, running linter..."
  npm run lint || echo "⚠️  No lint script found in package.json"
else
  echo "ℹ️  No ESLint config found, skipping..."
fi
```

The workflow checks if you have ESLint configured. If you do, it runs, if you don't, it skips gracefully. This means the workflow works for everyone regardless of their linting setup.

---

### **Job 2: Security Audit**

```yaml
security:
  name: 🔒 Security Audit
  runs-on: ubuntu-latest
```

This job scans your dependencies for known security vulnerabilities.

**The audit process:**

```bash
npm audit --audit-level=moderate
```

This checks all your npm packages against a vulnerability database and reports issues rated "moderate" severity or higher.

**Understanding** `continue-on-error: true`:

```yaml
continue-on-error: true
```

We don't fail the entire build if vulnerabilities are found. Instead, we report them so you can fix them. Here's why:

**Practical reality:**

* Many vulnerabilities are low-risk for backend APIs
    
* Some "vulnerabilities" only affect browser environments (not relevant for Strapi)
    
* Blocking every build for minor issues would slow you down too much
    

**What we do instead:**

* Report all vulnerabilities clearly
    
* Count high vs moderate severity issues
    
* Let you decide urgency based on context
    
* Encourage fixes without blocking development
    

**The detailed report:**

```bash
HIGH=$(cat audit-report.json | grep -o '"severity":"high"' | wc -l || echo "0")
MODERATE=$(cat audit-report.json | grep -o '"severity":"moderate"' | wc -l || echo "0")

echo "- **High**: $HIGH" >> $GITHUB_STEP_SUMMARY
echo "- **Moderate**: $MODERATE" >> $GITHUB_STEP_SUMMARY
```

You get a clear count of vulnerabilities in the workflow summary. If you see high-severity issues, you should probably fix them before deploying.

---

### **Job 3: Build Verification**

```yaml
build:
  name: 🏗️ Build Verification
  runs-on: ubuntu-latest
  needs: [lint, security]
```

This job verifies your Docker image builds successfully. The `needs: [lint, security]` means it only runs if both previous jobs pass.

**The build step:**

```yaml
- name: 🐳 Build Docker image (test only)
  uses: docker/build-push-action@v6
  with:
    context: .
    file: ./Dockerfile.stg
    push: false
    tags: test-build:latest
    platforms: linux/amd64
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

**Key settings:**

* `push: false` - **This is critical.** We build the image but don't push it anywhere
    
* `tags: test-build:latest` - Just a local tag, not used outside this workflow
    
* `platforms: linux/amd64` - Builds for DigitalOcean's architecture
    
* `cache-from/cache-to: type=gha` - Uses GitHub Actions cache for faster builds
    

**Why not push to GHCR?**

As mentioned earlier, pushing every feature branch to GHCR clutters your registry. This approach:

* ✅ Verifies your code compiles into a valid Docker image
    
* ✅ Uses caching to keep builds fast (2-5 minutes)
    
* ✅ Keeps your registry clean
    
* ✅ Saves bandwidth and storage
    

The CD workflow (Part 5b) will build and push to GHCR only when actually deploying.

**If you want to push certain branches:**

Add this condition to enable pushing for specific branches:

```yaml
- name: 🐳 Build and push Docker image
  uses: docker/build-push-action@v6
  with:
    context: .
    file: ./Dockerfile.stg
    push: ${{ github.ref == 'refs/heads/dev' }}  # Only push dev branch
    tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
    platforms: linux/amd64
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

This would push only the `dev` branch to GHCR. But for most use cases, the build-only approach is cleaner.

---

### **Job 4: Summary Jobs**

```yaml
ci-success:
  name: ✅ CI Complete
  needs: [lint, security, build]
  if: success()
```

These jobs provide clear feedback about the overall CI status.

**Success job:**

* Runs only if all previous jobs passed
    
* Creates a nice summary with green checkmarks
    
* Shows branch and commit information
    
* Confirms the code is safe to deploy
    

**Failure job:**

```yaml
ci-failure:
  name: ❌ CI Failed
  needs: [lint, security, build]
  if: failure()
```

* Runs only if any previous job failed
    
* Creates a summary explaining what went wrong
    
* Makes it obvious the code needs fixes
    
* Exits with error code to mark the workflow as failed
    

**Why these summary jobs matter:**

When you look at your commits on GitHub, you see:

* Green checkmark → All CI checks passed
    
* Red X → Something failed (click to see what)
    

These summary jobs make that status immediately clear without digging through logs.

---

## **Step 3: Commit and Push the Workflow**

Now let's activate the workflow:

```bash
# Make sure you're on dev branch
git checkout dev

# Add the workflow file
git add .github/workflows/ci.yml

# Commit
git commit -m "Add CI pipeline with security scanning and build verification"

# Push to dev branch
git push origin dev
```

**What happens next:**

1. GitHub receives your push
    
2. Detects the workflow file
    
3. Starts running the CI pipeline
    
4. You can watch it live in GitHub Actions tab
    

**Important Note About Workflow Visibility:**

Your CI workflow will automatically run as soon as you push it because of the `push` trigger we configured. Once it runs for the first time, you'll see it appear in the Actions tab on GitHub.

*Note: If you were creating a* `workflow_dispatch` (manually triggered) workflow instead, you'd need to merge it to the default branch first for the "Run workflow" button to appear. But since our CI workflow triggers automatically on push/pull\_request events, you don't need to worry about this - it'll just work immediately.

---

## **Step 4: Monitor Your First Build**

Let's watch the pipeline run for the first time.

### **View the Workflow Run**

1. Go to your GitHub repository
    
2. Click on **"Actions"** tab
    
3. You should see "🔄 Continuous Integration" running
    

### **What You'll See**

The workflow page shows:

* Overall status (queued, running, success, failed)
    
* Individual job status (lint, security, build, summary)
    
* Real-time logs
    
* Time taken for each step
    

Click on the workflow run to see detailed logs.

### **Job Progress**

**Job 1 - Code Quality (~1-2 minutes):**

```bash
📥 Checkout code
🔧 Setup Node.js
📦 Install dependencies
🔍 Run ESLint (if configured)
📊 Lint summary
```

If you don't have ESLint configured, you'll see:

```bash
ℹ️  No ESLint config found, skipping...
```

That's perfectly fine - the job still passes.

**Job 2 - Security Audit (~2-3 minutes):**

```bash
📥 Checkout code
🔧 Setup Node.js
📦 Install dependencies
🔍 Run security audit
📊 Generate detailed report
```

If you have no vulnerabilities:

```bash
✅ No security vulnerabilities found!
```

If you have some:

```bash
⚠️  Security vulnerabilities detected - check details above
- High: 0
- Moderate: 3
```

**Job 3 - Build Verification (~5-10 minutes first time):**

```bash
📥 Checkout code
🛠️ Set up Docker Buildx
🐳 Build Docker image (test only)
✅ Build successful
```

The first build takes longer because there's no cache. Subsequent builds use cached layers and finish in 2-5 minutes.

**Job 4 - Summary:**

```bash
🎉 All checks passed

✅ Continuous Integration Passed!

All validation checks completed successfully:
- ✅ Code quality check
- ✅ Security audit
- ✅ Build verification

Branch: dev
Commit: abc1234...
This code is safe to deploy! 🚀
```

### **Understanding Build Times**

**First build:** 10-15 minutes total

* Code quality: 2 minutes
    
* Security audit: 3 minutes
    
* Build verification: 8 minutes (no cache)
    
* Summary: instant
    

**Subsequent builds:** 5-7 minutes total

* Code quality: 2 minutes
    
* Security audit: 2 minutes (dependency cache)
    
* Build verification: 2-3 minutes (Docker cache)
    
* Summary: instant
    

The workflow gets faster as caches build up.

---

## **Step 5: Testing the CI Pipeline**

Let's create a feature branch and watch CI in action.

### **Create a Feature Branch**

```bash
# Create a new feature branch
git checkout -b feature/test-ci

# Make a small change
echo "# CI Pipeline Active" >> README.md

# Commit and push
git add README.md
git commit -m "Add CI status to README"
git push -u origin feature/test-ci
```

### **Watch the Green Checkmark Appear**

1. Go to your repository on GitHub
    
2. Click on **"Commits"** or look at your branch
    
3. You'll see a yellow circle while CI runs
    
4. After 5-10 minutes, it becomes:
    
    * Green checkmark (✅) if all checks passed
        
    * Red X (❌) if something failed
        

**Click on the checkmark/X** to see detailed results without leaving the commits page.

### **Create a Pull Request**

```bash
# Go to GitHub UI
# Click "Compare & pull request" for your feature branch
# Create the PR
```

On the PR page, you'll see:

```bash
✅ All checks have passed
3 successful checks
```

Or if something failed:

```bash
❌ Some checks were not successful
1 failing check
```

This makes code review way easier, reviewers know the code at least compiles before they start reviewing logic.

---

## **Step 6: Understanding Check Details**

Let's look at what each check tells you.

### **Code Quality Results**

If you have ESLint configured and it finds issues:

```bash
Line 45:  'userName' is assigned a value but never used  no-unused-vars
Line 102: Expected '===' but got '=='                    eqeqeq
```

Fix these before merging. Clean code = fewer bugs.

### **Security Audit Results**

If vulnerabilities are found:

```bash
## 🔒 Security Audit Results
⚠️  Security vulnerabilities detected

### Vulnerability Details:
- High: 2
- Moderate: 5

found 7 vulnerabilities (5 moderate, 2 high) in 234 scanned packages
```

**What to do:**

```bash
# Try automatic fixes first
npm audit fix

# If that doesn't work, update specific packages
npm update package-name

# Commit the fixes
git add package.json package-lock.json
git commit -m "Fix security vulnerabilities"
git push
```

CI will run again with the fixed dependencies.

### **Build Verification Results**

If the build fails:

```bash
❌ Build Verification Failed

ERROR: failed to solve: failed to read dockerfile: open Dockerfile.stg: no such file
```

**Common build failures:**

1. **Dockerfile not found** - Check filename and location
    
2. **npm install fails** - Check package.json syntax
    
3. **Build fails** - Check TypeScript/build errors
    
4. **Out of memory** - Simplify build or use multi-stage better
    

The logs show exactly where the build failed, making it easy to fix.

---

## **Extending the Workflow**

The workflow we built covers the essentials: code quality, security, and build verification. But you can add much more depending on your needs.

### **Common Additions**

**1\. Unit Tests**

If you have tests in your Strapi project:

```yaml
test:
  name: 🧪 Run Tests
  runs-on: ubuntu-latest
  needs: lint
  
  steps:
    - name: Checkout code
      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 tests
      run: npm test
```

Then update the build job to depend on tests:

```yaml
build:
  needs: [lint, security, test]
```

**2\. Code Coverage**

Track how much of your code is tested:

```yaml
- name: Generate coverage report
  run: npm run test:coverage

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    file: ./coverage/coverage-final.json
```

**3\. TypeScript Type Checking**

If using TypeScript:

```yaml
- name: Type check
  run: npm run type-check
```

**4\. Format Checking (Prettier)**

Enforce consistent code formatting:

```yaml
- name: Check formatting
  run: npx prettier --check "src/**/*.{js,ts,json}"
```

**5\. Dependency Review**

Check for license issues or outdated dependencies:

```yaml
- name: Check outdated dependencies
  run: npm outdated || true
```

**6\. Performance Budgets**

Ensure bundle sizes don't grow too large:

```yaml
- name: Check bundle size
  run: npm run build && du -sh build/
```

### **When to Add More Checks**

**Start simple** (what we have now):

* Code quality (ESLint)
    
* Security audit
    
* Build verification
    

**Add as needed:**

* Unit tests when you write tests
    
* Code coverage when tests are comprehensive
    
* Type checking if using TypeScript
    
* Format checking if team has formatting wars
    

**Don't add everything at once.** Start with basics, add more as your project and team grow.

---

## **Common Issues and Fixes**

Here are problems you might encounter:

### **Issue: Workflow doesn't trigger**

**Symptom:** You push code but no workflow runs

**Diagnosis:**

```bash
# Check you're on a tracked branch
git branch --show-current

# Verify workflow file exists
ls -la .github/workflows/ci.yml

# Check if it's committed
git log --oneline | head -5
```

**Solution:**

```bash
# Make sure workflow is committed
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push

# Check GitHub Actions tab for any error messages
```

### **Issue: Security audit reports vulnerabilities**

**Symptom:** Job shows warnings about vulnerable packages

**Solution:**

```bash
# Attempt automatic fixes
npm audit fix

# If fixes aren't available
npm audit fix --force  # May update to newer major versions

# Or update specific packages
npm update package-name

# Verify fixes
npm audit

# Commit changes
git add package.json package-lock.json
git commit -m "Fix security vulnerabilities"
git push
```

**When to ignore:**

Some vulnerabilities are safe to ignore:

* Development-only dependencies (testing tools, etc.)
    
* Vulnerabilities that don't affect server-side code
    
* False positives
    

Use your judgment, but don't ignore critical/high severity issues in production dependencies.

### **Issue: ESLint job fails even with no config**

**Symptom:** The lint job fails despite having `continue-on-error: true`

**Solution:**

This shouldn't happen, but if it does:

```bash
# Add a basic .eslintrc.json
cat > .eslintrc.json << 'EOF'
{
  "extends": ["eslint:recommended"],
  "env": {
    "node": true,
    "es2021": true
  },
  "parserOptions": {
    "ecmaVersion": 12
  }
}
EOF

# Add lint script to package.json
# "scripts": {
#   "lint": "eslint ."
# }

# Commit
git add .eslintrc.json package.json
git commit -m "Add ESLint configuration"
git push
```

### **Issue: Build is extremely slow (&gt;20 minutes)**

**Diagnosis:**

Check if caching is working:

```bash
# Look in workflow logs for:
# "Cache restored from key: ..."
```

**Solution:**

If cache isn't working, verify these settings in your workflow:

```yaml
cache-from: type=gha
cache-to: type=gha,mode=max
```

Also check:

* First build is always slow (10-15 minutes)
    
* Subsequent builds should be 2-5 minutes
    
* If consistently slow, your Dockerfile might not be optimized for caching
    

**Optimize Dockerfile for caching:**

```dockerfile
# Good - dependencies cached separately
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# Bad - everything rebuilds on any change
COPY . .
RUN npm ci
```

### **Issue: Jobs run out of order**

**Symptom:** Build job starts before security job finishes

**Solution:**

This shouldn't happen with the `needs:` directive, but if it does:

```yaml
# Make sure build job has this:
build:
  needs: [lint, security]  # Waits for both jobs
```

### **Issue: Pull request shows "Some checks haven't completed yet" forever**

**Symptom:** PR shows pending checks that never finish

**Solution:**

Usually a workflow configuration issue:

```bash
# Check Actions tab for stuck workflows
# Cancel any stuck runs manually
# Push a new commit to retrigger
```

If this happens repeatedly, there might be a syntax error in your workflow file. GitHub's workflow validator will show errors in the Actions tab.

---

## **Understanding GitHub Actions Costs**

Let's talk about what this actually costs you.

### **For Public Repositories:**

**Completely free.** Unlimited minutes. No cost whatsoever.

### **For Private Repositories:**

**Free tier:**

* 2,000 minutes/month
    
* Shared across all private repos
    
* Resets monthly
    

**Our typical usage:**

* Code quality: ~2 minutes
    
* Security audit: ~2 minutes
    
* Build verification: ~3 minutes (with cache)
    
* Summary: instant
    
* **Total per run: ~7 minutes**
    

**Monthly estimate:**

* 10 feature branches × 3 pushes each × 7 minutes = 210 minutes
    
* 20 PR commits × 7 minutes = 140 minutes
    
* **Total: ~350 minutes/month**
    

You'd need **285+ CI runs per month** to exceed the free tier. That's roughly 10 runs per day. If you're pushing that often, you might need to limit the CI workflow to important branches only (like main and dev), or you'll have to pay for the extra minutes.

### **If You Exceed Free Tier:**

GitHub charges $0.008 per minute for private repos.

* 3,000 minutes/month = $8/month extra
    
* That's still cheaper than most CI services
    

*For a staging environment working on 2-5 features simultaneously, you'll stay well within the free tier.*

---

## **What We've Accomplished**

Let's recap what your CI pipeline now does:

**Automated Validation:**

* ✅ Runs on every push to main, dev, feature, and fix branches
    
* ✅ Checks code quality with ESLint (if configured)
    
* ✅ Scans for security vulnerabilities
    
* ✅ Verifies Docker image builds successfully
    
* ✅ Provides clear pass/fail status on every commit
    

**Developer Experience:**

* ✅ See green checkmark or red X immediately
    
* ✅ Catch issues before code review
    
* ✅ Know code compiles before merging
    
* ✅ Get detailed feedback on what's wrong
    

**Clean Registry Management:**

* ✅ Builds verify code works
    
* ✅ Doesn't clutter GHCR with test images
    
* ✅ Keeps storage costs minimal
    
* ✅ CD workflow (Part 5b) handles actual deployments
    

**Professional Workflow:**

* ✅ Industry-standard CI practices
    
* ✅ Works for solo developers and teams
    
* ✅ Extensible for your specific needs
    
* ✅ Free for public repos, affordable for private
    

**And you're still at $6/month** for your DigitalOcean infrastructure. The CI pipeline is free (or nearly free for private repos).

---

## **What's Missing (Coming in Part 5b)**

Right now, you have automatic validation but still deploy manually:

1. Merge PR to dev
    
2. SSH into server
    
3. Pull new image (wait, we're not pushing images yet!)
    
4. Rebuild and restart
    
5. Hope nothing breaks
    

**Part 5b will add:**

* **Automated Docker image building and pushing to GHCR** (only when deploying)
    
* **Automated deployment** to your DigitalOcean server
    
* **Manual approval gates** (you control when deployments happen)
    
* **Health checks** (verify deployment succeeded)
    
* **Automatic rollbacks** (revert if deployment fails)
    
* **Deployment scripts** (single command to deploy or rollback)
    
* **Complete CD pipeline** from approved merge to running server
    

The CI part we built today is the foundation - it ensures your code is valid and buildable. Part 5b completes the pipeline by actually deploying that validated code.

---

## **A Note on the Deployment Step**

You might have noticed we validate code but don't push images to GHCR. Here's why we're saving that for Part 5b:

**The Philosophy:**

CI should answer: "Is this code good?"

* Does it compile? ✅
    
* Are there security issues? ⚠️
    
* Does it pass quality checks? ✅
    

CD should answer: "Let's deploy this code."

* Build production image
    
* Push to registry
    
* Deploy to server
    
* Verify it works
    

**Separating concerns:**

* CI runs on every branch (frequent, lightweight)
    
* CD runs on approved merges (infrequent, intentional)
    
* This keeps your registry clean and deploys deliberate
    

In Part 5b, we'll build and push to GHCR as part of the deployment workflow, not the validation workflow.

---

## **Quick Reference**

### **Workflow File Location:**

```bash
.github/workflows/ci.yml
```

### **Branches That Trigger CI:**

* `main`
    
* `dev`
    
* `feature/**` (all feature branches)
    
* `fix/**` (all fix branches)
    
* Pull requests to `main` or `dev`
    

### **Add More Branch Patterns:**

```yaml
on:
  push:
    branches:
      - main
      - dev
      - 'feature/**'
      - 'fix/**'
      - 'hotfix/**'     # Add this
      - 'release/**'    # Or this
```

### **View Workflow Runs:**

1. Repository → Actions tab
    
2. Click on workflow run
    
3. Click on job name
    
4. Expand steps to see details
    

### **Trigger Workflow Manually:**

1. Actions tab
    
2. Select "🔄 Continuous Integration"
    
3. Click "Run workflow"
    
4. Choose branch
    
5. Click "Run workflow"
    

---

## **What's Next: Part 5b Preview**

In the next (and final!) article of this series, we're completing the automation:

**What Part 5b Will Cover:**

**Deployment Automation:**

* Setting up GitHub Environments (staging environment config)
    
* Adding SSH deployment keys securely
    
* Creating deployment scripts on your server
    
* Building the CD workflow with image push to GHCR
    
* Implementing manual approval gates
    
* Adding health checks and monitoring
    
* Creating rollback procedures
    
* Testing the complete end-to-end pipeline
    

**The Final Result:**

After completing Part 5b, you'll have TWO deployment workflow options:

**Option 1 - Auto-Deploy on Merge (Recommended for most teams):**

1. Push feature code → CI validates (green checkmark)
    
2. Create PR → CI runs again on PR
    
3. Merge to dev → CD workflow triggers automatically
    
4. Click "Approve Deployment" in GitHub
    
5. Automatic build → Push to GHCR → Deploy to staging
    
6. Health check confirms deployment worked
    
7. If something breaks → One-click rollback
    

**Option 2 - Manual-Dispatch Workflow (Great for small teams & testing):**

1. Push your code to ANY branch → CI validates
    
2. Go to Actions tab → Click "Run workflow"
    
3. Select which branch to deploy from (dev, feature, hotfix, etc.)
    
4. Enter version (or auto-generate)
    
5. Deployment runs immediately (no approval needed)
    

**Why small teams love this:**

* Deploy from any branch without merging first (perfect for testing)
    
* No approval gates needed (you're already being intentional by clicking "Run")
    
* Great for solo developers or tight-knit teams
    
* Test feature branches in your dev environment before merging to staging/main
    
* Emergency hotfixes when you need speed
    

Most larger teams use Option 1 for regular development (safety with approval gates), while smaller teams often prefer Option 2 for its flexibility. Many teams keep both - Option 1 for normal workflow, Option 2 for testing and emergencies.

You'll go from manual deployments to a professional CI/CD pipeline with full control, safety nets, and automatic validation at every step.

---

**Final File Structure After Part 5a:**

```bash
your-strapi-project/
├── .github/
│   └── workflows/
│       └── ci.yml                    # CI pipeline (new!)
├── src/                              # Your Strapi code
├── config/
├── public/
├── Dockerfile.stg                   # From Part 1
├── docker-compose.stg.yml            # From Part 2
├── .env.stg                          # From Part 2
├── package.json
├── package-lock.json
├── .gitignore
├── .eslintrc.json                    # Optional
└── README.md
```

---

*Got questions about the CI setup or running into issues with the workflow? Drop a comment and I'll help troubleshoot. Next week, we're wrapping up the series with the CD pipeline and deployment automation - the final piece that completes the puzzle!*
