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
61 changes: 61 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# ----------------------------
# Backend (FastAPI) - runtime
# ----------------------------
# Used by backend OAuth + GitHub sync on startup (GitHub OAuth endpoints).
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret

# Used to sign/verify JWT access tokens issued by the API.
# WARNING: Replace this with a long, random secret (>= 32 chars / 256 bits).
# Example only (DO NOT use in production).
JWT_SECRET=unsafe-example-jwt-secret-please-change

# Database URL for SQLAlchemy async engine.
# Examples:
# Postgres:
# postgresql+asyncpg://USER:PASSWORD@HOST:5432/DB
# SQLite:
# sqlite+aiosqlite:///./local.db
#
# Used by:
# - `docker-compose.yml` via `DATABASE_URL`
# - `deploy.yml` database migration job (`alembic upgrade head`)
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/solfoundry

# Redis connection string.
# Used by:
# - `backend/app/services/websocket_manager.py` (Redis pub/sub for websockets)
REDIS_URL=redis://localhost:6379/0

# ----------------------------
# Frontend (Vite) - build-time hints
# ----------------------------
# Used by some pages to construct absolute API URLs.
# Many API calls use relative paths (`/api/...`), so this may be optional for some deployments.
VITE_API_URL=http://localhost:8000
VITE_WS_URL=ws://localhost:8000

# ----------------------------
# Docker Compose (local) - DB
# ----------------------------
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=solfoundry

# ----------------------------
# CI/CD (GitHub Actions) - deployment secrets
# ----------------------------
# These values must be set as GitHub repository secrets.

# deploy.yml -> Vercel
VERCEL_TOKEN=your_vercel_token
VERCEL_ORG_ID=your_vercel_org_id
VERCEL_PROJECT_ID=your_vercel_project_id

# deploy.yml -> DigitalOcean Kubernetes
DIGITALOCEAN_ACCESS_TOKEN=your_digitalocean_access_token
DIGITALOCEAN_CLUSTER_NAME=your_digitalocean_cluster_name

# deploy.yml -> frontend build injection
# Used by the `deploy.yml` frontend build step (currently wired into the build environment).
API_URL=https://api.solfoundry.org
Comment on lines +46 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The deployment secret guidance is not environment-safe.

Line 48 tells maintainers to configure these as repository-level secrets, and Line 61 even defaults API_URL to the production API. For a pipeline that now supports both staging and production, that guidance makes it easy for preview/staging deployments to build against production endpoints or infrastructure instead of isolated environment-specific values.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 52-52: [UnorderedKey] The VERCEL_ORG_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)


[warning] 53-53: [UnorderedKey] The VERCEL_PROJECT_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)


[warning] 61-61: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 46 - 61, The .env.example currently instructs
maintainers to set repository-level secrets and hardcodes API_URL to production
which risks staging/preview builds hitting prod; update the file to remove or
replace the production default for API_URL, clarify that VERCEL_TOKEN,
VERCEL_ORG_ID, VERCEL_PROJECT_ID, DIGITALOCEAN_ACCESS_TOKEN,
DIGITALOCEAN_CLUSTER_NAME and API_URL must be configured as environment-specific
secrets (e.g., production vs staging/preview) in CI and deployment configs, add
placeholder names for staging (e.g., API_URL_STAGING) and a short note to wire
deploy.yml to use the appropriate environment-specific secret rather than a
global production value.

147 changes: 145 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,87 @@ jobs:
run: cargo fmt -- --check
continue-on-error: true

# ==================== CONTAINER IMAGE BUILD CHECKS ====================
container-image-build:
name: Container Image Build (backend + frontend)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build backend image
run: docker build -t solfoundry-backend:test ./backend

- name: Build frontend image
run: docker build -t solfoundry-frontend:test ./frontend

# ==================== DOCKER COMPOSE SMOKE TEST ====================
# Spins up the full local stack and verifies basic readiness.
compose-smoke-test:
name: Compose Smoke Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Start stack (db + redis + backend + frontend)
env:
JWT_SECRET: smoke-test-jwt-secret-at-least-32-characters
GITHUB_CLIENT_ID: smoke-test-client-id
GITHUB_CLIENT_SECRET: smoke-test-client-secret
run: |
docker compose up -d --build db redis backend frontend

- name: Wait for health endpoints + verify DB/Redis/proxy/auth paths
run: |
set -euo pipefail
for i in {1..30}; do
if curl -fsS http://localhost:8000/health >/dev/null 2>&1; then
break
fi
sleep 2
done

BACKEND_HEALTH="$(curl -fsS http://localhost:8000/health)"
echo "$BACKEND_HEALTH"
BACKEND_HEALTH="$BACKEND_HEALTH" python - <<'PY'
import json, os
health = json.loads(os.environ["BACKEND_HEALTH"])
assert health.get("database") == "ok", "database is not healthy"
assert health.get("redis") == "ok", "redis is not healthy"
assert health.get("status") == "ok", "overall backend health is not ok"
print("backend health verified (database + redis)")
PY

curl -fsS http://localhost:8081/health
# Verify that nginx -> backend proxying works.
curl -fsS "http://localhost:8081/api/leaderboard?limit=1" >/dev/null

# Minimal authenticated path exercise: protected endpoint should reject unauthenticated requests.
CODE="$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/auth/me)"
test "$CODE" = "401"

- name: Exercise backend schema initialization
run: |
docker compose exec -T backend python -c "import asyncio; from app.database import init_db; asyncio.run(init_db())"

- name: Ensure compose services are healthy
run: |
set -euo pipefail
docker inspect --format '{{json .State.Health.Status}}' solfoundry-db | rg '"healthy"'
docker inspect --format '{{json .State.Health.Status}}' solfoundry-redis | rg '"healthy"'
docker inspect --format '{{json .State.Health.Status}}' solfoundry-backend | rg '"healthy"'
docker inspect --format '{{json .State.Health.Status}}' solfoundry-frontend | rg '"healthy"'

- name: Tear down
if: always()
run: docker compose down -v

# ==================== SUMMARY ====================
ci-status:
name: CI Status Summary
runs-on: ubuntu-latest
needs: [backend-lint, backend-tests, frontend-lint, frontend-typecheck, frontend-tests, frontend-build, contracts-check, rust-lint]
needs: [backend-lint, backend-tests, frontend-lint, frontend-typecheck, frontend-tests, frontend-build, contracts-check, rust-lint, container-image-build, compose-smoke-test, backend-integration-postgres]
if: always()
steps:
- name: Check CI Results
Expand All @@ -277,7 +353,74 @@ jobs:
echo "| Frontend Build | ${{ needs.frontend-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Contracts Check | ${{ needs.contracts-check.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Rust Lint | ${{ needs.rust-lint.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Container Image Build | ${{ needs.container-image-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Compose Smoke Test | ${{ needs.compose-smoke-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Backend Integration (Postgres) | ${{ needs.backend-integration-postgres.result }} |" >> $GITHUB_STEP_SUMMARY

- name: Fail if any job failed
if: contains(needs.*.result, 'failure')
run: exit 1
run: exit 1

# ==================== BACKEND POSTGRES INTEGRATION ====================
backend-integration-postgres:
name: Backend Integration (Postgres)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
options: >-
--health-cmd="pg_isready -U test -d test"
--health-interval=10s
--health-timeout=5s
--health-retries=10
env:
PYTHONPATH: .
TEST_DATABASE_URL: postgresql+asyncpg://test:test@postgres:5432/test
SECRET_KEY: test-secret-key-for-ci
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install Dependencies
working-directory: backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio

- name: Wait for Postgres
run: |
for i in {1..30}; do
if python - <<'PY'
import os, sys, time
import asyncpg, asyncio

async def main():
url = os.environ["TEST_DATABASE_URL"]
try:
conn = await asyncpg.connect(url)
await conn.close()
return True
except Exception:
return False

print(asyncio.run(main()))
PY
then
break
fi
sleep 2
done

- name: Run DB-backed health test
working-directory: backend
run: pytest tests/test_logging_and_errors.py::test_health_check_format -v
Loading
Loading