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"

Series Navigation:
Part 0: Introduction - Why This Setup?
Part 1: Containerizing Strapi v5
Part 2: Deploying to DigitalOcean
Part 3: Production Web Server Setup
Part 4: Automated Database Backups
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:
mainbranchdevbranchfeature/**branchesfix/**branchesAny pull requests targeting
mainordev
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 auditto check for vulnerable dependenciesReports 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/directoryDefines 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/*, andfix/*branchesRuns on pull requests targeting
mainordevThe
'feature/**'pattern matchesfeature/user-auth,feature/payment-system, etc.The
'fix/**'pattern matchesfix/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 anywheretags: test-build:latest- Just a local tag, not used outside this workflowplatforms: linux/amd64- Builds for DigitalOcean's architecturecache-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:
GitHub receives your push
Detects the workflow file
Starts running the CI pipeline
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
Go to your GitHub repository
Click on "Actions" tab
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
Go to your repository on GitHub
Click on "Commits" or look at your branch
You'll see a yellow circle while CI runs
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:
Dockerfile not found - Check filename and location
npm install fails - Check package.json syntax
Build fails - Check TypeScript/build errors
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:
Merge PR to dev
SSH into server
Pull new image (wait, we're not pushing images yet!)
Rebuild and restart
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:
maindevfeature/**(all feature branches)fix/**(all fix branches)Pull requests to
mainordev
Add More Branch Patterns:
on:
push:
branches:
- main
- dev
- 'feature/**'
- 'fix/**'
- 'hotfix/**' # Add this
- 'release/**' # Or this
View Workflow Runs:
Repository → Actions tab
Click on workflow run
Click on job name
Expand steps to see details
Trigger Workflow Manually:
Actions tab
Select "🔄 Continuous Integration"
Click "Run workflow"
Choose branch
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):
Push feature code → CI validates (green checkmark)
Create PR → CI runs again on PR
Merge to dev → CD workflow triggers automatically
Click "Approve Deployment" in GitHub
Automatic build → Push to GHCR → Deploy to staging
Health check confirms deployment worked
If something breaks → One-click rollback
Option 2 - Manual-Dispatch Workflow (Great for small teams & testing):
Push your code to ANY branch → CI validates
Go to Actions tab → Click "Run workflow"
Select which branch to deploy from (dev, feature, hotfix, etc.)
Enter version (or auto-generate)
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!





