Skip to main content

Command Palette

Search for a command to run...

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

Part 5a of "Building a Complete Deployment Environment for Strapi v5: A Practical Series"

Updated
26 min read
CI/CD Pipeline Part 1: Automated Builds and Security Scanning with GitHub Actions

Series Navigation:

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

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

Create the CI Workflow File

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

Paste this complete workflow:

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

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:

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)

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

This job checks your code quality using ESLint.

The smart part:

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

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

This job scans your dependencies for known security vulnerabilities.

The audit process:

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:

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:

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

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:

- 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:

- 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

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:

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:

# 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):

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

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

ℹ️  No ESLint config found, skipping...

That's perfectly fine - the job still passes.

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

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

If you have no vulnerabilities:

✅ No security vulnerabilities found!

If you have some:

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

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

📥 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:

🎉 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

# 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

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

On the PR page, you'll see:

✅ All checks have passed
3 successful checks

Or if something failed:

❌ 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:

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:

## 🔒 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:

# 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:

❌ 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:

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:

build:
  needs: [lint, security, test]

2. Code Coverage

Track how much of your code is tested:

- 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:

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

4. Format Checking (Prettier)

Enforce consistent code formatting:

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

5. Dependency Review

Check for license issues or outdated dependencies:

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

6. Performance Budgets

Ensure bundle sizes don't grow too large:

- 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:

# 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:

# 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:

# 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:

# 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 (>20 minutes)

Diagnosis:

Check if caching is working:

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

Solution:

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

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:

# 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:

# 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:

# 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:

.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:

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:

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!

Building a Complete Deployment Environment for Strapi v5: A Practical Series

Part 2 of 7

Learn how to build a production-ready, budget-friendly staging environment for Strapi v5 using Docker, DigitalOcean, and modern DevOps practices. Complete with automated backups and CI/CD.

Up next

Automated Database Backups for Strapi v5: AWS S3 Setup

Part 4 of "Building a Complete Deployment Environment for Strapi v5: A Practical Series"

Building a GitHub Actions CI Pipeline for Strapi v5: Security, Testing