Basic CI Explaination
What Even Is a CI Pipeline? (And Why You Should Care)
Before writing a single line of YAML, you need to understand what you're actually automating.
A CI (Continuous Integration) pipeline is a sequence of automated steps that runs every time your code changes. Its purpose is straightforward:
Catch problems early so broken code never reaches production.
Think of It Like Airport Security
Imagine every commit you make is like a bag at an airport.
Before it gets on the plane (production), it goes through multiple security checks. Some pass instantly, some get flagged, and some are rejected entirely.
A CI pipeline works the same way—every change is validated before it’s allowed to move forward.
The Core Mental Model
At its core, every CI pipeline follows a predictable flow:
Trigger → Checkout → Build → Test → Scan → Package → Push → Deploy → Verify
This isn’t tied to any specific tool. Whether you're using GitHub Actions, GitLab CI, or Jenkins, the idea remains the same.
Each stage exists for a reason:
It starts with a trigger (like a push or pull request) Pulls your code into a runner (checkout) Builds your application (build) Validates functionality (test) Checks for vulnerabilities (scan) Versions the artifact (package) Publishes it (push) Ships it (deploy) And finally confirms everything worked (verify)
Some pipelines skip or combine steps, but the backbone rarely changes.
Anatomy of a GitHub Actions Pipeline
Every workflow in GitHub Actions has three essential parts:
name:
on:
jobs: : runs-on: steps: - <step 1> - <step 2> What this means: name → The label you see in the Actions tab on → Defines when the pipeline runs (push, pull_request, etc.) jobs → The actual execution units
Each job runs on a fresh virtual machine. By default, jobs run in parallel unless explicitly chained using needs: .
A Real Example: Tetris App CI/CD Pipeline
Here’s a production-style pipeline for a Dockerized app:
name: Tetris CI/CD Pipeline
on: push: branches: ["main"]
jobs: build-test-push: runs-on: ubuntu-latest
steps:
# 1. Checkout — always first
- name: Checkout code
uses: actions/checkout@v4
# 2. Scan source code BEFORE building
- name: Trivy FS Scan
run: |
docker run --rm \
-v ${{ github.workspace }}:/app \
aquasec/trivy fs /app \
--severity HIGH,CRITICAL \
--exit-code 1
# 3. Build Docker image
- name: Build image
run: docker build -t tetris-app .
# 4. Scan the built image
- name: Trivy Image Scan
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image tetris-app \
--severity HIGH,CRITICAL \
--exit-code 1
# 5. Test the container
- name: Test container
run: |
docker run -d -p 8080:80 --name test-container tetris-app
sleep 5
curl -f http://localhost:8080
docker stop test-container && docker rm test-container
# 6. Login to Docker Hub
- name: Login to Docker Hub
run: echo "\({{ secrets.DOCKER_PASSWORD }}" | docker login -u "\){{ secrets.DOCKER_USERNAME }}" --password-stdin
# 7. Tag image
- name: Tag image
run: |
docker tag tetris-app \({{ secrets.DOCKER_USERNAME }}/tetris-app:\){{ github.sha }}
docker tag tetris-app ${{ secrets.DOCKER_USERNAME }}/tetris-app:latest
# 8. Push image
- name: Push image
run: |
docker push \({{ secrets.DOCKER_USERNAME }}/tetris-app:\){{ github.sha }}
docker push ${{ secrets.DOCKER_USERNAME }}/tetris-app:latest
Key Decisions Explained Why scan before building?
Scanning your source code early helps catch:
hardcoded secrets insecure configurations known vulnerabilities
It’s fast and prevents wasting time building unsafe code.
Why scan the image again?
Even if your code is clean, vulnerabilities can come from:
base images OS packages dependencies
This step acts as the real security gate.
Why use --exit-code 1?
Without it, the pipeline will continue even if vulnerabilities are found.
With it:
Any HIGH or CRITICAL issue immediately fails the pipeline.
This is what turns scanning into enforcement.
Why tag with commit SHA?
${{ github.sha }}
This ensures every build is:
unique traceable reproducible
If something breaks in production, you can map it directly to the exact commit.
Why push at the end?
Pushing is a side effect—you can’t undo it easily.
So you only push when everything else has passed:
build
scans
tests
This makes your pipeline a reliable safety gate.
Before You Run This Pipeline
You’ll need to configure two GitHub repository secrets:
DOCKER_USERNAME
DOCKER_PASSWORD (use an access token, not your actual password)
Navigate to:
Settings → Secrets and variables → Actions → New repository secret
Final Thought
A CI pipeline isn’t just about automation.
It’s about confidence.
When your pipeline is solid, you stop worrying about deployments breaking things—and start focusing on building.
