Skip to content

Migrate Python tooling to UV and Ty #1

Migrate Python tooling to UV and Ty

Migrate Python tooling to UV and Ty #1

Workflow file for this run

name: backend
on:
workflow_dispatch:
push:
branches: main
paths:
- "src/app/**"
- "src/Dockerfile"
- "pyproject.toml"
- "uv.lock"
- "docker/*"
- "Makefile"
- ".env.example"
pull_request:
branches: main
paths:
- "src/app/**"
- "src/Dockerfile"
- "pyproject.toml"
- "uv.lock"
- "docker/*"
- "Makefile"
- ".env.example"
env:
UV_VERSION: "0.11.14"
PYTHON_VERSION: "3.11"
jobs:
build:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
activate-environment: true
- uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-backend-${{ hashFiles('uv.lock') }}
- name: Install deps
run: |
make install-backend
- name: Build docker image
run: make build-backend
start:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Load images
run: |
make build-backend
docker compose -f docker-compose.dev.yml pull db localstack
docker image ls -a
- name: Start docker
id: start-docker
continue-on-error: true
run: docker compose -f docker-compose.dev.yml up -d backend --wait
- name: Generate debug logs
if: steps.start-docker.outcome == 'failure'
run: |
docker compose -f docker-compose.dev.yml logs backend --tail=100 > build-logs.txt
cat build-logs.txt
- uses: actions/upload-artifact@v4
if: steps.start-docker.outcome == 'failure'
with:
name: build-docker-logs
path: build-logs.txt
- name: Exit the workflow
if: steps.start-docker.outcome == 'failure'
run: |
echo "Docker startup failed. Logs saved as artifact build-docker-logs"
exit 1
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Run ruff
run: |
make install-quality
uv run --group quality ruff --version
make ruff-check
ty:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Run ty
run: |
make install-quality
uv run --group quality ty --version
make typing-check
prek-hooks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Run prek hooks
run: |
make install-quality
git checkout -b temp
prek install
prek --version
make precommit
deps-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Run dependency sync checker
run: make deps-check
tests:
needs: start
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Load images & start containers
run: UV_GROUPS="server test" docker compose -f docker-compose.dev.yml up -d backend --wait --build
- name: Run pytest
run: |
docker compose -f docker-compose.dev.yml exec -T backend pytest --cov=app --cov-report xml tests/
docker compose -f docker-compose.dev.yml cp backend:/app/coverage.xml ./coverage-backend.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend
fail_ci_if_error: true
migrations:
needs: start
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Load images & start containers
run: docker compose -f docker-compose.dev.yml up -d db localstack --wait
- name: Run alembic migrations
run: |
docker compose -f docker-compose.dev.yml run --rm backend sh -c " \
echo '--- Checking current revision ---' && \
alembic current && \
echo '--- Showing history ---' && \
alembic history --verbose && \
echo '--- Downgrading one revision ---' && \
alembic downgrade -1 && \
echo '--- Upgrading to head ---' && \
alembic upgrade head && \
echo '--- Migration cycle successful ---' \
"
publish-image:
needs: [build, tests, migrations]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
- name: Load images
run: |
make build-backend
docker image ls -a
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push to GitHub container registry
run: docker push ghcr.io/${{ github.repository }}:latest
deploy:
needs: publish-image
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.SSH_PROD_HOST }}
username: ${{ secrets.SSH_PROD_USER }}
key: ${{ secrets.SSH_DEPLOY_KEY }}
source: "docker-compose.prod.yml"
target: ./
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_PROD_HOST }}
username: ${{ secrets.SSH_PROD_USER }}
key: ${{ secrets.SSH_DEPLOY_KEY }}
script: |
# Ensure we have max disk space
docker rmi -f $(docker images -f "dangling=true" -q)
docker volume rm -f $(docker volume ls -f "dangling=true" -q)
# Pull services
docker compose -f docker-compose.prod.yml pull
# Stop service
if [ -f "docker-compose.yml" ]; then
docker compose down --remove-orphans
fi
# Update compose + .env
mv docker-compose.prod.yml docker-compose.yml
if [ -f ".env" ]; then
mv .env .env.prev
fi
touch .env
# DB Container
echo "POSTGRES_DB=${{ secrets.POSTGRES_DB }}" >> .env
echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env
echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env
echo "POSTGRES_PORT=${{ secrets.POSTGRES_PORT }}" >> .env
# Backend Container
# DB Init
echo "SUPERADMIN_EMAIL=${{ secrets.SUPERADMIN_EMAIL }}" >> .env
echo "SUPERADMIN_PWD=${{ secrets.SUPERADMIN_PWD }}" >> .env
# Auth
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
# Storage
echo "S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" >> .env
echo "S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" >> .env
echo "S3_REGION=${{ secrets.S3_REGION }}" >> .env
echo "S3_ENDPOINT_URL=${{ secrets.S3_ENDPOINT_URL }}" >> .env
echo "S3_DISABLE_INTEGRITY_CHECK=${{ secrets.S3_DISABLE_INTEGRITY_CHECK }}" >> .env
# Email
echo "RESEND_KEY=${{ secrets.RESEND_KEY }}" >> .env
echo "EMAIL_FROM=${{ secrets.EMAIL_FROM }}" >> .env
echo "BACKEND_HOST=${{ secrets.BACKEND_HOST }}" >> .env
echo "RESEND_VERIFY_API_KEY=false" >> .env
# Middlewares
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "SERVER_NAME=${{ secrets.SERVER_NAME }}" >> .env
echo "LOGFIRE_TOKEN=${{ secrets.LOGFIRE_TOKEN }}" >> .env
# Others
echo "SUPPORT_EMAIL=${{ secrets.SUPPORT_EMAIL }}" >> .env
# Traefik Container
echo "TRAEFIK_BACKEND_HOST=${{ secrets.TRAEFIK_BACKEND_HOST }}" >> .env
echo "ACME_EMAIL=${{ secrets.ACME_EMAIL }}" >> .env
# Restart services
docker compose up -d --wait
# Check it's up to date
docker inspect -f '{{ .Created }}' $(docker compose images -q backend)
- name: Ping server
run: sleep 5 && curl ${{ secrets.PROD_ENDPOINT }}