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)
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.
The FCA Support Agent ships two workflows:
.github/workflows/ci.yml — lint (Ruff), type-check (mypy), test (pytest with coverage) gated on a Postgres + Redis service container.github/workflows/deploy.yml — Build + push web/frontend container images to GHCR, then SSH into the target server to pull and restart# .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
# .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
# .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
--health-cmd — GitHub Actions supports healthcheck-based gating; the runner waits until Postgres (pg_isready) and Redis (redis-cli ping) report ready before running tests. Without this, tests race pip install against DB startup and produce flaky failures.postgres+asyncpg URL inside CI — Mirrors prod; tests that use a sync URL hide async-specific bugs (asyncpg.OperationalError: too many connections).pip install -r requirements.txt instead of pip install -e . — The lockfile (pip-compile → requirements.txt) is the source of truth for prod; PRs must NOT introduce unconstrained deps. Use -e only for packages that change alongside this repo.docker compose pull && up — Always pull so the local cache doesn’t serve a stale image tag. The prune -f cleans dangling images so disk usage doesn’t grow unbounded on long-lived deploy hosts.requirements.txt change. Pipe requirements.txt hash into actions/cache@v4 key; otherwise stale cache ships broken installs in CI.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.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=<new-tag> --record for rolling updates. Add a canary step that ships to 10% of pods via Argo Rollouts and waits for the SLO.
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.
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.
.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_requestname: 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:
- 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
actions/checkout@v4 — clone the repo.actions/setup-python@v5 — install Python 3.11 + cache pip packages for speed.black --check, isort --check, flake8 enforce style.mypy app/ static analysis.coverage.xml artifact to codecov.io.Any failure stops the merge button. Coverage drop below threshold fails the build.
deploy.yml — triggered on tag pushname: 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 && up -d. Manual deploys are still possible via the Actions UI.
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.
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.
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.
A: Re-tag the previous working image as :latest and ssh into the VPS: docker compose pull && docker compose up -d. Add a “rollback” job in Actions that’s a one-click button.
package-lock.json hash instead of HEAD.