Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Secrets
.env
.env.*
!.env.example

# Version control
.git
.github

# IDE
.vscode
.idea

# Dependencies (installed in container)
node_modules
frontend/node_modules

# Caches and build artifacts
**/__pycache__
**/.mypy_cache
**/.pytest_cache
**/htmlcov
**/.coverage
**/.ruff_cache

# Documentation and config
docs/
*.md
!backend/README.md
45 changes: 0 additions & 45 deletions .env

This file was deleted.

57 changes: 57 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ─── Domain & Routing ────────────────────────────────────────────────────────
# Domain used by Traefik for routing and TLS certificates
DOMAIN=localhost
# STACK_NAME=app # Docker stack name prefix

# ─── Environment ─────────────────────────────────────────────────────────────
# Options: local | staging | production
ENVIRONMENT=local

# ─── Supabase (required) ──────────────────────────────────────────────────────
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-supabase-service-key

# ─── Clerk Authentication (required) ─────────────────────────────────────────
CLERK_SECRET_KEY=sk_test_your-clerk-secret-key
# CLERK_JWKS_URL= # Optional: override the Clerk JWKS endpoint URL
# CLERK_AUTHORIZED_PARTIES= # Optional: comma-separated authorized parties

# ─── Backend ──────────────────────────────────────────────────────────────────
SERVICE_NAME=my-service
SERVICE_VERSION=0.1.0
BACKEND_CORS_ORIGINS=http://localhost,http://localhost:5173
# API_V1_STR=/api/v1 # Default: /api/v1
# HTTP_CLIENT_TIMEOUT=30 # HTTP client timeout in seconds
# HTTP_CLIENT_MAX_RETRIES=3 # HTTP client retry count

# ─── Logging ──────────────────────────────────────────────────────────────────
# LOG_LEVEL options: DEBUG | INFO | WARNING | ERROR
LOG_LEVEL=INFO
# LOG_FORMAT options: json | console
LOG_FORMAT=json

# ─── Frontend ─────────────────────────────────────────────────────────────────
# WITH_UI=false # Set to true to enable frontend services
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_FRONTEND=frontend
# TAG=latest # Docker image tag

# ─── Observability ────────────────────────────────────────────────────────────
SENTRY_DSN=
# GIT_COMMIT= # Set automatically by CI (git commit SHA)
# BUILD_TIME= # Set automatically by CI (build timestamp)

# ─── Deployment (CI/CD) ─────────────────────────────────────────────────────
# These variables are used by GitHub Actions deployment workflows.
# GHCR authentication uses GITHUB_TOKEN (automatic in GitHub Actions).
#
# Platform-specific deploy tokens (uncomment one for your platform):
# RAILWAY_TOKEN= # Railway deploy token
# RAILWAY_SERVICE_ID_STAGING= # Railway service ID for staging
# RAILWAY_SERVICE_ID_PRODUCTION= # Railway service ID for production
# GCP_SA_KEY= # Google Cloud service account key (JSON)
# GCP_SERVICE_NAME= # Google Cloud Run service name
# FLY_API_TOKEN= # Fly.io API token
# DEPLOY_HOST= # Self-hosted: SSH host for deployment
# ALIBABA_ACCESS_KEY= # Alibaba Cloud access key
# ALIBABA_SECRET_KEY= # Alibaba Cloud secret key
18 changes: 0 additions & 18 deletions .github/workflows/add-to-project.yml

This file was deleted.

116 changes: 116 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: CI
# Unified CI workflow replacing test-backend.yml + test-docker-compose.yml

on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize

jobs:
backend-lint:
name: Backend Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync
working-directory: backend
- name: Ruff check
run: uv run ruff check app/
working-directory: backend
- name: Ruff format check
run: uv run ruff format --check app/
working-directory: backend
- name: Mypy
run: uv run mypy app
working-directory: backend

backend-test:
name: Backend Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync
working-directory: backend
- name: Run tests
run: uv run coverage run -m pytest tests/unit/ tests/integration/ -v
working-directory: backend
env:
SUPABASE_URL: "http://localhost:54321"
SUPABASE_SERVICE_KEY: "test-service-key"
CLERK_SECRET_KEY: "test-clerk-key"
ENVIRONMENT: "local"
- name: Coverage report
run: uv run coverage report --fail-under=90
working-directory: backend
- name: Coverage HTML
run: uv run coverage html
working-directory: backend
- name: Store coverage files
uses: actions/upload-artifact@v6
with:
name: coverage-html
path: backend/htmlcov
include-hidden-files: true

frontend-ci:
name: Frontend Lint & Build
runs-on: ubuntu-latest
# Only run if frontend/ directory exists
if: hashFiles('frontend/package.json') != ''
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
working-directory: frontend
- name: Lint
run: bun run lint
working-directory: frontend
- name: Build
run: bun run build
working-directory: frontend

docker-build:
name: Docker Build
runs-on: ubuntu-latest
needs: [backend-lint, backend-test]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build backend image
run: docker build -t test-backend . -f backend/Dockerfile

# Branch protection gate
alls-green:
name: CI Complete
runs-on: ubuntu-latest
needs: [backend-lint, backend-test, docker-build, frontend-ci]
if: always()
steps:
- name: Check all jobs
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: frontend-ci
103 changes: 81 additions & 22 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,90 @@ name: Deploy to Production

on:
release:
types:
- published
types: [published]

concurrency:
group: deploy-production
cancel-in-progress: false

jobs:
deploy:
# Do not deploy in the main repository, only in user projects
if: github.repository_owner != 'fastapi'
runs-on:
- self-hosted
- production
env:
ENVIRONMENT: production
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Verify staging image exists in GHCR
env:
IMAGE: ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
run: |
docker manifest inspect "${IMAGE}" > /dev/null || \
(echo "ERROR: Staging image not found for SHA ${{ github.sha }}. Ensure the staging deploy completed before publishing a release." && exit 1)

- name: Promote staging image to production (re-tag, no rebuild)
env:
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
# Pull the exact image built and validated in the staging pipeline
docker pull "ghcr.io/${REPO}/backend:${SHA}"

# Re-tag as version and latest
docker tag "ghcr.io/${REPO}/backend:${SHA}" \
"ghcr.io/${REPO}/backend:${TAG_NAME}"
docker tag "ghcr.io/${REPO}/backend:${SHA}" \
"ghcr.io/${REPO}/backend:latest"

# Push version and latest tags
docker push "ghcr.io/${REPO}/backend:${TAG_NAME}"
docker push "ghcr.io/${REPO}/backend:latest"

# --- PLUGGABLE DEPLOY STEP ---
# Uncomment ONE of the following blocks for your platform.
# Use ${{ github.event.release.tag_name }} as the production image tag.
# The image is identical to what was validated on staging — no rebuild.

# --- Railway ---
# - name: Deploy to Railway
# run: railway up --service ${{ secrets.RAILWAY_SERVICE_ID_PRODUCTION }}
# env:
# RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

# --- Alibaba Cloud (ACR + ECS) ---
# - name: Push to Alibaba Cloud ACR
# run: |
# docker tag ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }} \
# registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.event.release.tag_name }}
# docker push registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.event.release.tag_name }}
# - name: Deploy to ECS
# run: aliyun ecs ... # Update ECS service with new image

# --- Google Cloud Run ---
# - name: Deploy to Cloud Run
# uses: google-github-actions/deploy-cloudrun@v2
# with:
# service: ${{ secrets.GCP_SERVICE_NAME }}
# image: ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }}

# --- Fly.io ---
# - name: Deploy to Fly.io
# uses: superfly/flyctl-actions/setup-flyctl@main
# - run: flyctl deploy --image ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }}
# env:
# FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

# --- Self-hosted (Docker Compose via SSH) ---
# - name: Deploy via SSH
# run: |
# ssh ${{ secrets.DEPLOY_HOST }} "docker pull ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }} && docker compose up -d"
Loading
Loading