CI/CD — GitHub Actions Automated Delivery Pipeline
CI/CD is like an automated assembly line in an auto factory—parts (code) come in, go through welding (lint), quality inspection (testing), and painting (security scanning), and the finished products (Docker images) that pass inspection are automatically delivered to the dealership (production environment).
1. What You'll Learn
- GitHub Actions Basics: Workflow, Job, Step, and Action Concepts
- CI pipeline: lint → pytest → security scan → Docker build
- CD Pipeline: Image Push → Docker Hub/GHCR → Server Deployment
- Environment Management: Branch Strategy for dev / staging / production
- Alice Scenario: Bob submits a PR → automated testing → Charlie merges → automated deployment
2. Alice's True Story
(1) Pain Point: Manual deployment often leads to errors
After Bob submitted the code, Alice manually fetched the code from the server, ran tests, built the image, and restarted the container. This process took 30 minutes each time, and she often skipped test steps—on one occasion, Alice forgot to run the migrations, and the new endpoint reported a 500 error online. Charlie said he could perform these tasks manually in each of the three environments (dev, staging, and prod), but the risk was too high.
(2) Solution using GitHub Actions
GitHub Actions automatically run the full pipeline—lint → test → build → deploy—with every push or pull request. All steps are defined by code, so nothing can be overlooked, and failures automatically block merges.
(3) Revenue
Deployment time has been reduced from 30 minutes (manual) to 5 minutes (automated), and the issue of missed tests has been completely eliminated (PRs that fail tests cannot be merged), ensuring that deployments across all three environments are completely consistent.
3. GitHub Actions Basics
(1) Core Concepts
flowchart LR
Trigger[Trigger: push/PR] --> Workflow[Workflow]
Workflow --> Job1[Job 1: Lint+Test]
Workflow --> Job2[Job 2: Build]
Job1 --> Step1[Step: ruff check]
Job1 --> Step2[Step: pytest]
Job2 --> Step3[Step: docker build]
Job2 --> Step4[Step: docker push]
| Concept | Description | Example |
|---|---|---|
| Workflow | Automated Process Definition | .github/workflows/ci.yml |
| Trigger | Trigger Condition | push, pull_request |
| Job | Execution unit consisting of a set of steps | test, build, deploy |
| Step | Single Operation | run: pytest |
| Action | Reusable Actions | actions/checkout@v4 |
| Runner | Runtime Environment | ubuntu-latest |
(2) Branch and Environment Mapping
graph TD
Feature[feature/*] -->|PR| Develop[develop]
Develop -->|Deploy| DevEnv[Dev Environment]
Develop -->|PR| Main[main]
Main -->|Deploy| Staging[Staging Environment]
Main -->|Tag v*| Prod[Production Environment]
| Branch | Environment | Trigger | Deployment Method |
|---|---|---|---|
feature/* |
- | PR Automated Testing | Not Deployed |
develop |
Dev | Auto-push | Docker Compose |
main |
Staging | Auto-push | Docker Compose |
v* tag |
Production | tag Manual | Docker Compose / K8s |
4. CI Pipeline
(1) ▶ Example: Complete CI Workflow
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install UV
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install dependencies
run: uv sync --frozen
- name: Run ruff lint
run: uv run ruff check app/ tests/
- name: Run ruff format check
run: uv run ruff format --check app/ tests/
test:
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: pricetracker
POSTGRES_PASSWORD: test_password
POSTGRES_DB: pricetracker_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install UV
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install dependencies
run: uv sync --frozen
- name: Run pytest
env:
DATABASE_URL: postgresql+asyncpg://pricetracker:test_password@localhost:5432/pricetracker_test
REDIS_URL: redis://localhost:6379/0
SECRET_KEY: test-secret-key-for-ci
run: uv run pytest tests/ -v --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
security:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- name: Run safety check
run: |
pip install safety
safety check --json || true
- name: Run bandit
run: |
pip install bandit
bandit -r app/ -f json || true
build:
runs-on: ubuntu-latest
needs: [test, security]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: false
tags: pricetracker:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Output:
ext CI/CD pipeline loaded Pipeline status: passed Tests: 12 passed, 0 failed
5. CD Pipeline
(1) ▶ Example: Production Deployment Workflow
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
tags:
- 'v*'
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/pricetracker
docker compose pull
docker compose up -d --remove-orphans
docker compose exec api alembic upgrade head
echo "Deployed version ${{ github.ref_name }}"
Output:
CONTAINER ID IMAGE STATUS PORTS
abc123 nginx:latest Up 2 hours 0.0.0.0:80->80/tcp
(2) Complete CI/CD Pipeline Workflow
flowchart LR
Push[Bob pushes code] --> Lint[Lint: ruff]
Lint --> Test[Test: pytest]
Lint --> Sec[Security: safety + bandit]
Test --> Build[Docker Build]
Sec --> Build
Build -->|On tag| Push[Push to GHCR]
Push --> Deploy[Deploy to Production]
Deploy --> Health[Health Check]
Health --> Done[✓ Live]
(2) ▶ Example: Branch Protection and Environmental Protection Rules
# .github/workflows/branch-protection.yml
name: Branch Protection Check
on:
pull_request:
types: [opened, synchronize]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Verify PR target is main
run: |
if [ "${{ github.base_ref }}" != "main" ]; then
echo "PR must target main branch"
exit 1
fi
- name: Check environment approval
if: github.event.pull_request.merged == true
uses: octokit/request-action@v2
with:
route: POST /repos/{owner}/{repo}/deployments
environment: staging
required_reviewers: 1
Output:
CI/CD pipeline loaded
Pipeline status: passed
Tests: 12 passed, 0 failed
❓ FAQ
Base.metadata.create_all() to create tables directly (without Alembic). Run alembic upgrade head in the CD deployment script.SERVER_SSH_KEY, DB_PASSWORD, and others are stored here, and you can reference them in YAML using ${{ secrets.XXX }}.on: push: tags: ['v*'] to trigger deployment only when a tag is created. On the development branch, run tests only—do not deploy.cache-from: type=gha to leverage GitHub Actions caching. The initial build takes 5 minutes; subsequent changes only rebuild the modified layers, taking about 1-2 minutes.docker compose down + docker compose pull <previous-tag> + docker compose up -d. Keep the image tag from the previous version to facilitate rollback.📖 Summary
- GitHub Actions consists of a four-level structure: Workflow → Job → Step → Action
- CI pipeline: lint (ruff) → test (pytest + PostgreSQL/Redis service containers) → security → build
- CD pipeline: tag trigger → build Docker image → push to GHCR → deploy to server via SSH
- Branch strategy: feature PR for testing, develop for Dev deployment, main for Staging deployment, v* tags for Production deployment
- Docker layer caching + GitHub Actions caching to speed up builds; Secrets for secure key management
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create
.github/workflows/ci.yml. Set it up so thatruff checkandpytestrun automatically when you push or create a PR, and verify that the GitHub Actions page shows a green "Passed." Hint:on: push: branches: [main] - Advanced Exercise (Difficulty ⭐⭐): Add PostgreSQL and Redis service containers to the test job, configure environment variables so that pytest can connect to the test database, and add a Docker build step to verify that the image builds successfully. Hint:
services:+options: --health-cmd - Challenge (Difficulty: ⭐⭐⭐): Complete CI/CD—The CI pipeline includes linting, testing, security checks, and building; the CD pipeline pushes the image to GHCR when the
v*tag is created and deploys it to the server via SSH, using GitHub Secrets to store the keys. Hint:docker/login-action+appleboy/ssh-action+${{ secrets.XXX }}
---|



