Skip to content
Open
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
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/backend"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
53 changes: 53 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Backend CI

on:
pull_request:
paths:
- "backend/**"
- ".github/workflows/backend-ci.yml"
push:
branches:
- main
paths:
- "backend/**"
- ".github/workflows/backend-ci.yml"

concurrency:
group: backend-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
env:
DJANGO_ENV: development
DJANGO_DEBUG: "true"
DJANGO_SECRET_KEY: ci-insecure-secret-key
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: backend/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Django check
run: python manage.py check

- name: Migrations check
run: python manage.py makemigrations --check --dry-run

- name: Run tests
run: python manage.py test
68 changes: 68 additions & 0 deletions .github/workflows/due-reminders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Due Reminders

on:
schedule:
- cron: "*/10 * * * *"
workflow_dispatch:

jobs:
send-reminders:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
env:
DJANGO_ENV: production
DJANGO_DEBUG: "false"
DJANGO_SECRET_KEY: ${{ secrets.PROD_DJANGO_SECRET_KEY }}
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
ALLOWED_HOSTS: ${{ secrets.PROD_ALLOWED_HOSTS }}
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: ${{ secrets.PROD_EMAIL_HOST }}
EMAIL_PORT: ${{ secrets.PROD_EMAIL_PORT }}
EMAIL_USE_TLS: "true"
EMAIL_HOST_USER: ${{ secrets.PROD_EMAIL_HOST_USER }}
EMAIL_HOST_PASSWORD: ${{ secrets.PROD_EMAIL_HOST_PASSWORD }}
DEFAULT_FROM_EMAIL: ${{ secrets.PROD_DEFAULT_FROM_EMAIL }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Validate required secrets
shell: bash
run: |
required=(
DJANGO_SECRET_KEY
DATABASE_URL
ALLOWED_HOSTS
EMAIL_HOST
EMAIL_PORT
EMAIL_HOST_USER
EMAIL_HOST_PASSWORD
DEFAULT_FROM_EMAIL
)
missing=0
for name in "${required[@]}"; do
if [ -z "${!name}" ]; then
echo "::error::Missing required secret-derived value: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: backend/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run due reminders command
run: python manage.py send_due_reminders
4 changes: 2 additions & 2 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v4
Expand All @@ -49,7 +49,7 @@ jobs:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Secret Scan

on:
pull_request:
push:
branches:
- main
- master

jobs:
gitleaks:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 changes: 72 additions & 0 deletions .github/workflows/prod-migrate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Production Migrate

on:
workflow_dispatch:

jobs:
migrate:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
env:
DJANGO_ENV: production
DJANGO_DEBUG: "false"
DJANGO_SECRET_KEY: ${{ secrets.PROD_DJANGO_SECRET_KEY }}
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
ALLOWED_HOSTS: ${{ secrets.PROD_ALLOWED_HOSTS }}
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: ${{ secrets.PROD_EMAIL_HOST }}
EMAIL_PORT: ${{ secrets.PROD_EMAIL_PORT }}
EMAIL_USE_TLS: "true"
EMAIL_HOST_USER: ${{ secrets.PROD_EMAIL_HOST_USER }}
EMAIL_HOST_PASSWORD: ${{ secrets.PROD_EMAIL_HOST_PASSWORD }}
DEFAULT_FROM_EMAIL: ${{ secrets.PROD_DEFAULT_FROM_EMAIL }}
SENTRY_DSN: ${{ secrets.PROD_SENTRY_DSN }}
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.PROD_SENTRY_TRACES_SAMPLE_RATE }}
CHANNEL_REDIS_URL: ${{ secrets.PROD_CHANNEL_REDIS_URL }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Validate required secrets
shell: bash
run: |
required=(
DJANGO_SECRET_KEY
DATABASE_URL
ALLOWED_HOSTS
EMAIL_HOST
EMAIL_PORT
EMAIL_HOST_USER
EMAIL_HOST_PASSWORD
DEFAULT_FROM_EMAIL
)
missing=0
for name in "${required[@]}"; do
if [ -z "${!name}" ]; then
echo "::error::Missing required secret-derived value: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: backend/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run migrations
run: python manage.py migrate --noinput

- name: Verify core.0023
run: python manage.py showmigrations core | grep "\[X\] 0023"
20 changes: 20 additions & 0 deletions .github/workflows/prod-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Production Smoke

on:
workflow_dispatch:

jobs:
smoke:
runs-on: ubuntu-latest
steps:
- name: Health endpoint
run: |
curl --fail --silent --show-error "${{ secrets.PROD_BACKEND_HEALTH_URL }}"

- name: Frontend homepage
run: |
curl --fail --silent --show-error "${{ secrets.PROD_FRONTEND_URL }}"

- name: Frontend manifest
run: |
curl --fail --silent --show-error "${{ secrets.PROD_FRONTEND_URL }}/manifest.json"
10 changes: 10 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "Boardly Gitleaks Config"

[allowlist]
description = "Allow local/dev placeholders that are not real credentials."
regexes = [
'''replace-with-long-random-secret''',
'''ci-insecure-secret-key''',
'''your-login''',
'''your-password''',
]
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ For authenticated smoke tests, set:
- Backend uses ASGI (`daphne`) for HTTP + WebSocket support.
- Frontend production build is served via `nginx`.
- CI workflow lives in `.github/workflows/frontend-ci.yml`.
- Backend CI workflow lives in `.github/workflows/backend-ci.yml`.
- Secret scan workflow lives in `.github/workflows/gitleaks.yml`.

### Realtime Config (Redis + WS reconnect)
- Backend channel layer:
Expand All @@ -76,6 +78,35 @@ For authenticated smoke tests, set:
- `REACT_APP_WS_MAX_RECONNECT_MS`
- `REACT_APP_WS_RECONNECT_JITTER_MS`

### Observability
- Backend Sentry:
- `SENTRY_DSN`
- `SENTRY_TRACES_SAMPLE_RATE`
- Frontend Sentry:
- `REACT_APP_SENTRY_DSN`
- `REACT_APP_SENTRY_ENV`
- `REACT_APP_SENTRY_TRACES_SAMPLE_RATE`
- Health endpoints:
- `GET /api/health/`
- `GET /healthz/`

### Security Baseline
- Rotate all leaked/legacy SMTP credentials and update production secrets.
- Dependabot config: `.github/dependabot.yml`.
- Secret scanning in CI: `gitleaks` workflow.
- For GitHub branch protection (requires repository admin), run:
- `pwsh scripts/github/apply-branch-protection.ps1`
- To enable GitHub secret scanning/push protection (requires repository admin), run:
- `pwsh scripts/github/enable-security-analysis.ps1`
- To set Fly production runtime secrets (Sentry/WS + optional SMTP), run:
- `pwsh scripts/fly/set-prod-secrets.ps1`
- To run Fly production migrations and verify `core.0023`, run:
- `pwsh scripts/fly/run-prod-migrations.ps1`
- Production deploy checklist:
- `docs/ops/deploy-checklist.md`
- Sentry alert templates:
- `docs/ops/sentry-alert-rules.md`

## License
MIT License. See `LICENSE`.

Expand Down
7 changes: 6 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS=True
SECURE_HSTS_PRELOAD=True
SECURE_PROXY_SSL_HEADER_ENABLED=True

# Observability
SENTRY_DSN=
SENTRY_TRACES_SAMPLE_RATE=0.1

# PostgreSQL (recommended for production)
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require

Expand All @@ -20,7 +24,8 @@ DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require
# If empty, backend falls back to in-memory layer (single instance/dev only).
CHANNEL_REDIS_URL=redis://default:password@host:6379/0

# SMTP
# SMTP (required in production when EMAIL_BACKEND is SMTP)
# Never commit real credentials to git.
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
Expand Down
Loading