Skip to Content

GitHub Actions CI/CD — Lint, Type, Test, Seed, Deploy

Data Flow

GITHUB ACTIONS CI/CD  —  Data Flow

   PR opened  /  push main  /  release tag


   ──lint─────  black --check, isort --check, flake8

        ▼ passes
   ──test─────  pytest --cov=app

        ▼ passes
   ──build────  docker buildx  ─►  push ghcr.io/.../<image>:sha

        ▼ success
   ──deploy───  kubectl apply -f k8s/        (dev on push main)
                 helm upgrade fca-app …       (prod on release tag, manual approval)

What? (Concept Overview)

A CI/CD pipeline gates every PR on lint + type-check + tests against a real Postgres + Redis service container and ships every merge to OCI Container Registry on GitHub, then triggers a remote redeploy over SSH. Convention over configuration: maintainers review GitHub Actions YAML, not bespoke shell scripts. The pipeline is reproducible because service containers create ephemeral dependencies on every run.

Project Context

The FCA Support Agent ships two workflows:

How? (Quick Reference Blocks)

3.1 CI Workflow Skeleton

# .github/workflows/ci.yml
name: CI
 
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    services:
      postgres:
        image: pgvector/pgvector:pg15
        env:
          POSTGRES_USER: fca_user
          POSTGRES_PASSWORD: fca_password
          POSTGRES_DB: fca_test
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U fca_user"
          --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
      - uses: actions/setup-python@v5
        with: {python-version: "3.12"}
      - run: pip install -r requirements.txt
      - run: ruff check .                 # lint
      - run: mypy app                     # type-check
      - run: pytest -v --cov=app
        env:
          DATABASE_URL: postgresql+asyncpg://fca_user:fca_password@localhost:5432/fca_test
          REDIS_URL:    redis://localhost:6379/0

3.2 Deploy Workflow — Build & Push Multi-Image

# .github/workflows/deploy.yml
name: Deploy
 
on:
  push:
    branches: [main]
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build & push web image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/fca-multi-agent-support/fca-app:latest
 
      - name: Build & push frontend image
        uses: docker/build-push-action@v6
        with:
          context: ./frontend
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/fca-multi-agent-support/fca-frontend:latest

3.3 Trigger Remote Redeploy Over SSH

# .github/workflows/deploy.yml — tail
      - name: Trigger remote redeploy
        uses: appleboy/ssh-action@v1
        with:
          host:    ${{ secrets.DEPLOY_HOST }`}
          username: ${{ secrets.DEPLOY_SSH_USER }`}
          key:     ${{ secrets.DEPLOY_SSH_KEY }`}
          script: |
            cd ~/fca-multi-agent-support
            docker compose pull
            docker compose up -d
            docker image prune -f

Why? (Parameter Breakdown

Common Pitfalls

  1. Caching pip without a key that invalidates on requirements.txt change. Pipe requirements.txt hash into actions/cache@v4 key; otherwise stale cache ships broken installs in CI.
  2. Running tests without pytest-asyncio mode declared. Async tests in pytest need @pytest.mark.asyncio or asyncio: auto in pyproject.toml. CI fails with “coroutine was never awaited” without it.

Real-World Interview Prep

Q1: How would you migrate this single-VM deploy pipeline to Kubernetes?

A: Three migrations. (1) Replace SSH deploy step with kubectl apply -f k8s/ after build. (2) Move secrets to Kubernetes secrets bound from a sealed-secrets controller (Bitnami sealed-secrets, External Secrets Operator). (3) Replace docker compose pull with kubectl set image deployment/fca-web fca-web=&lt;new-tag> --record for rolling updates. Add a canary step that ships to 10% of pods via Argo Rollouts and waits for the SLO.

Q2: Your CI is slow. First three things you’d profile?

A: (1) pip install time → cache with actions/setup-python@v5 + cache: 'pip'. (2) Postgres fixture boot → reuse the service container across jobs. (3) Test discovery → split unit/integration tests; run unit tests on every PR, integration tests only on main. Beyond that, parallel matrix builds by Python version.

Q3: Why appleboy/ssh-action rather than fabric or ansible?

A: Native GitHub Actions integration; no Python runtime on the runner required; secret injection is built-in. fabric/ansible would require installing Python dependencies on the runner and managing credential storage separately. The trade-off: appleboy/ssh-action is great for one-line deploy scripts, but for complex multi-step orchestration move to Ansible or Pulumi.

Top-to-Bottom Code Walkthrough (.github/workflows/ci.yml + .github/workflows/deploy.yml)

CI/CD is the last line of defence. No PR merges without green tests; no production deploy without a passing CI.

ci.yml — triggered on push and pull_request

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: pgvector/pgvector:pg15
        env:
          POSTGRES_USER: fca_user
          POSTGRES_PASSWORD: fca_password
          POSTGRES_DB: fca_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U fca_user"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

GitHub Actions service containers spin up Postgres and Redis as sidecars. The CI test runner connects to them via localhost:5432 and localhost:6379 — same URLs the app uses.

Steps

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
  with:
    python-version: '3.11'
    cache: 'pip'
- name: Install dependencies
  run: pip install -r requirements.txt
- name: Lint
  run: |
    black --check app/
    isort --check app/
    flake8 app/
- name: Type-check
  run: mypy app/
- name: Test
  env:
    DATABASE_URL: postgresql+asyncpg://fca_user:fca_password@localhost:5432/fca_test
    REDIS_URL: redis://localhost:6379/0
  run: pytest --cov=app --cov-report=xml
- name: Upload coverage
  uses: codecov/codecov-action@v4

What blocks a PR

Any failure stops the merge button. Coverage drop below threshold fails the build.

deploy.yml — triggered on tag push

name: Deploy
on:
  push:
    tags: ['v*.*.*']
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build Docker image
      run: docker build -t fca-app:${{ github.ref_name }} .
    - name: Push to GHCR
      run: |
        echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        docker push ghcr.io/davidsandeep1996-spec/fca-multi-agent-support/fca-app:${{ github.ref_name }}
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
    - name: SSH to VPS
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_SSH_KEY }}
        script: |
          cd /opt/fca
          git pull
          docker compose pull
          docker compose up -d

Tag-triggered deploy — pushing v1.2.3 builds the image and sshes to the VPS to run docker compose pull &amp;&amp; up -d. Manual deploys are still possible via the Actions UI.

Common Pitfalls

Caching secrets in workflow logs — the secrets.* reference is automatically redacted; never echo $GITHUB_TOKEN plain.

Skipping actions/setup-python cache: 'pip' — without cache, install takes 3 minutes instead of 30 seconds. Set cache key on requirements.txt hash.

Allowing merges with failing tests — branch protection on main requires ci.yml to pass. Don’t disable without a written exception.

Real-World Interview Prep

Q1: Why service containers instead of mocking databases?

A: Service containers run the real database. Mocking might miss connection-pool issues, race conditions, or SQL syntax errors. Real Postgres-in-CI gives high confidence the test passes in production too.

Q2: What’s the trade-off between GHCR (free, public) and private registries?

A: GHCR is free for public images. For private BFSI workloads, use AWS ECR / GCP Artifact Registry. Costs are minor but VPC isolation matters for compliance.

Q3: How do you roll back a bad deploy?

A: Re-tag the previous working image as :latest and ssh into the VPS: docker compose pull &amp;&amp; docker compose up -d. Add a “rollback” job in Actions that’s a one-click button.

🛠️ Production Failure Playbook

🚨 Failure 1 — Cache miss → 6-min install step every PR

🚨 Failure 2 — ECR auth expired → deployment hangs 20 min

🚨 Failure 3 — Helm upgrade race during HPA scale-down

Advertisement