Skip to content
Merged
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
100 changes: 100 additions & 0 deletions .do/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: generate-admin
region: nyc

databases:
- name: db
engine: PG
version: "17"
production: true
cluster_name: generate-admin-db

services:
- name: api
github:
repo: GenerateNU/admin
branch: main
deploy_on_push: true
source_dir: backend
dockerfile_path: backend/Dockerfile
instance_size_slug: apps-s-1vcpu-0.5gb
instance_count: 1
http_port: 8080
health_check:
http_path: /health
initial_delay_seconds: 15
period_seconds: 10
envs:
- key: DATABASE_URL
scope: RUN_AND_BUILD_TIME
value: postgresql+asyncpg://${db.USERNAME}:${db.PASSWORD}@${db.HOSTNAME}:${db.PORT}/${db.DATABASE}?sslmode=require
- key: APP_ENVIRONMENT
scope: RUN_AND_BUILD_TIME
value: production
- key: APP_LOG_LEVEL
scope: RUN_AND_BUILD_TIME
value: INFO
- key: ENTRA_TENANT_ID
scope: RUN_AND_BUILD_TIME
value: a8eec281-aaa3-4dae-ac9b-9a398b9215e7
- key: ENTRA_API_CLIENT_ID
scope: RUN_AND_BUILD_TIME
value: 6b24d4eb-4d4c-44c5-8252-cf13aa888eca
- key: CORS_ALLOWED_ORIGINS
scope: RUN_AND_BUILD_TIME
value: ${APP_URL}
- key: INITIAL_OWNER_EMAIL
scope: RUN_AND_BUILD_TIME
value: nguyen.mai4@northeastern.edu
- key: REDIS_URL
scope: RUN_AND_BUILD_TIME
type: SECRET
value: CHANGE_ME
- key: AWS_ACCESS_KEY_ID
scope: RUN_AND_BUILD_TIME
type: SECRET
value: CHANGE_ME
- key: AWS_SECRET_ACCESS_KEY
scope: RUN_AND_BUILD_TIME
type: SECRET
value: CHANGE_ME
- key: AWS_REGION
scope: RUN_AND_BUILD_TIME
value: us-east-1
- key: S3_ENDPOINT
scope: RUN_AND_BUILD_TIME
value: ""
- key: S3_BUCKET_NAME
scope: RUN_AND_BUILD_TIME
value: generate-admin
- key: S3_PUBLIC_BASE_URL
scope: RUN_AND_BUILD_TIME
value: https://generate-admin.s3.us-east-1.amazonaws.com

jobs:
- name: migrate
kind: PRE_DEPLOY
github:
repo: GenerateNU/admin
branch: main
deploy_on_push: true
source_dir: backend
dockerfile_path: backend/Dockerfile
instance_size_slug: apps-s-1vcpu-0.5gb
run_command: alembic upgrade head
envs:
- key: DATABASE_URL
scope: RUN_AND_BUILD_TIME
value: postgresql+asyncpg://${db.USERNAME}:${db.PASSWORD}@${db.HOSTNAME}:${db.PORT}/${db.DATABASE}?sslmode=require
- key: APP_ENVIRONMENT
scope: RUN_AND_BUILD_TIME
value: production
- key: ENTRA_TENANT_ID
scope: RUN_AND_BUILD_TIME
value: a8eec281-aaa3-4dae-ac9b-9a398b9215e7
- key: ENTRA_API_CLIENT_ID
scope: RUN_AND_BUILD_TIME
value: 6b24d4eb-4d4c-44c5-8252-cf13aa888eca
- key: REDIS_URL
scope: RUN_AND_BUILD_TIME
type: SECRET
value: CHANGE_ME
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000

INITIAL_OWNER_EMAIL=
INVITATION_TTL_HOURS=336
FRONTEND_BASE_URL=http://localhost:3000

RESEND_API_KEY=
RESEND_FROM_EMAIL=
77 changes: 36 additions & 41 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,45 +39,12 @@ jobs:
- name: Mypy
run: uv run mypy src

test:
name: test
contract:
name: api contract
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend

services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: generate_admin
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d generate_admin"
--health-interval 5s
--health-timeout 5s
--health-retries 10

redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10

env:
# Ports are the container defaults here, not the offset ones docker-compose
# publishes locally. Everything else falls back to its config default.
DATABASE_URL: postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/generate_admin
DATABASE_URL: postgresql+asyncpg://unused:unused@127.0.0.1:5432/unused
REDIS_URL: redis://127.0.0.1:6379/0
APP_ENVIRONMENT: local

steps:
- uses: actions/checkout@v4

Expand All @@ -86,11 +53,39 @@ jobs:
enable-cache: true
cache-dependency-glob: backend/uv.lock

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: uv sync --all-groups
run: |
uv sync --all-groups --directory backend
npm ci

- name: Regenerate schema and client
run: |
uv run --directory backend python -m admin.cli openapi
npm run gen

- name: Fail if the committed output is stale
run: |
if ! git diff --exit-code -- openapi.json packages/api/src/generated; then
echo "::error::openapi.json or the generated client is out of date. Run 'just gen' and commit."
exit 1
fi

test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run migrations
run: uv run alembic upgrade head
- name: Run tests
run: |
docker compose -f docker-compose.test.yml up --build \
--abort-on-container-exit --exit-code-from backend-test

- name: Pytest
run: uv run pytest
- name: Tear down
if: always()
run: docker compose -f docker-compose.test.yml down -v
48 changes: 48 additions & 0 deletions .github/workflows/publish-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Publish API client

on:
push:
tags:
- "api-v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.2.0)"
required: true

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"

- name: Resolve version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF_NAME#api-v}" >> "$GITHUB_OUTPUT"
fi

- name: Install dependencies
run: npm ci

- name: Set package version
run: npm version "${{ steps.version.outputs.value }}" --no-git-tag-version --workspace @generatenu/api

- name: Build
run: npm run build --workspace @generatenu/api

- name: Publish
run: npm publish --workspace @generatenu/api
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [uv](https://docs.astral.sh/uv/) (Python 3.12+)
- Docker, for Postgres / Redis / LocalStack
- [just](https://github.com/casey/just)
- Node 22+, for the frontend (orval requires >= 22.18); `nvm use` picks it up from `.nvmrc`

## Quickstart

Expand All @@ -20,6 +21,8 @@ just dev # http://localhost:8000

Check it with `curl localhost:8000/health`. API docs are at `/docs`.

For the Next.js admin console, see [`frontend/README.md`](frontend/README.md).

## Services

Docker compose uses offset host ports so it does not collide with anything already running.
Expand All @@ -41,6 +44,8 @@ Docker compose uses offset host ports so it does not collide with anything alrea
| `just rollback` | undo the last migration |
| `just revision <name>` | create a migration |
| `just seed` | sync roles and permissions |
| `just openapi` | write `openapi.json` (no server) |
| `just gen` | `openapi` + regenerate the TS client |
| `just test` | pytest |
| `just lint` | ruff check + format check |
| `just fmt` | ruff autofix + format |
Expand All @@ -61,3 +66,4 @@ uses a real Redis.
- test: Postgres and Redis service containers, migrations, then pytest

It mirrors `just check`.

8 changes: 8 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
.env
.env.local
59 changes: 59 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# syntax=docker/dockerfile:1
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never

WORKDIR /app

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev

COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev


FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS test

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never

WORKDIR /app

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --all-groups

COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --all-groups

ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

CMD ["sh", "-c", "alembic upgrade head && pytest"]


FROM python:3.12-slim-bookworm AS runtime

RUN groupadd --system app && useradd --system --gid app --home-dir /app app

WORKDIR /app
COPY --from=builder --chown=app:app /app /app
RUN rm -rf /app/tests

ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

USER app
EXPOSE 8080

CMD ["sh", "-c", "uvicorn admin.main:app --host 0.0.0.0 --port ${PORT:-8080}"]
1 change: 0 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ plugins = ["pydantic.mypy"]
strict = true
warn_return_any = false

# These ship without type stubs; everything else stays strict.
[[tool.mypy.overrides]]
module = ["asyncpg.*", "boto3.*", "botocore.*"]
ignore_missing_imports = true
Expand Down
Loading
Loading