Skip to content

ref(config): Migrate from Pydantic to dature for configuration loading #121

ref(config): Migrate from Pydantic to dature for configuration loading

ref(config): Migrate from Pydantic to dature for configuration loading #121

Workflow file for this run

name: CI
on:
push:
branches:
- '**'
tags-ignore:
- '**'
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.13"
TYPE_CHECK_PATHS: "src/"
LINT_PATHS: "src/ tests/"
# ── Validate ──────────────────────────────────────────────────────────────
jobs:
check-required-files:
name: Validate Required Files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check required files exist
run: |
missing=0
for f in .pre-commit-config.yaml pyproject.toml uv.lock; do
if [ ! -f "$f" ]; then
echo " - $f"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "❌ Missing required files (listed above)"
exit 1
fi
echo "✅ All required configuration files are present"
check-dependencies:
name: Validate Lockfile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Verify lockfile is up-to-date
run: uv lock --check
lint-and-format:
name: Lint & Format (Ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --frozen --group dev
- name: Ruff lint
run: uv run ruff check $LINT_PATHS --output-format=github
- name: Ruff format check
run: uv run ruff format $LINT_PATHS --check
# ── Test ──────────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: template_app
POSTGRES_USER: dev_user
POSTGRES_PASSWORD: dev_password
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_INITDB_ARGS: "--auth-host=trust"
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --frozen --group tests
- name: Run tests with coverage
env:
POSTGRES_TEST_HOST: localhost
POSTGRES_TEST_PORT: "5432"
run: |
uv run pytest tests/ \
-v --tb=short \
-n auto --dist worksteal \
--junitxml=pytest-junit.xml \
--cov=src \
--cov-report=xml:coverage.xml \
--cov-report=term-missing
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
pytest-junit.xml
coverage.xml
# ── Security ──────────────────────────────────────────────────────────────
# TODO: make proper security scan job with SAST, secret scanning, dependency scanning, etc. For now, these checks are included in lint and test jobs to keep things simple and fast by default.
# security-scan:
# name: Security Scan
# runs-on: ubuntu-latest
# continue-on-error: true
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-python@v5
# with:
# python-version: ${{ env.PYTHON_VERSION }}
# - uses: astral-sh/setup-uv@v4
# with:
# enable-cache: true
# cache-dependency-glob: "uv.lock"
# - name: Install dependencies
# run: uv sync --frozen --group dev
# - name: Ruff security rules
# run: uv run ruff check $LINT_PATHS --select=S --output-format=github || true
# - name: Check for potential secrets
# run: |
# echo "🔍 Checking for potential secrets (tracked YAML/JSON only)..."
# set +e
# git grep -nEi "password|secret|key|token" -- '*.yaml' '*.yml' '*.json' \
# | grep -v -Ei '(example|template)' \
# | grep -v '.github/workflows/'
# GREP_EXIT=$?
# set -e
# if [ "$GREP_EXIT" -eq 0 ]; then
# echo "⚠️ Potential secrets found (review output above)"
# exit 1
# else
# echo "✅ No obvious secrets found"
# fi
# ── Type Check ────────────────────────────────────────────────────────────
type-check:
name: Type Check (ty)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --frozen --group dev
- name: Run ty
run: uv run ty check $TYPE_CHECK_PATHS
# ── Build ─────────────────────────────────────────────────────────────────
# NOTE: disabled in template since not all projects will build a Docker image, and we want to keep CI fast by default. Can be enabled as needed.
# build-check:
# name: Build Check
# runs-on: ubuntu-latest
# needs: [lint-and-format, test, security-scan]
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-python@v5
# with:
# python-version: ${{ env.PYTHON_VERSION }}
# - uses: astral-sh/setup-uv@v4
# with:
# enable-cache: true
# cache-dependency-glob: "uv.lock"
# - name: Install production dependencies
# run: uv sync --frozen --all-packages --no-group dev
# - name: Prepare env file
# run: cp .env.example .env
# - name: Verify application imports
# run: uv run python -c "from src.presentation.api.app import create_app; print('✅ Application imports successfully')"
# build-image:
# name: Build & Push Docker Image
# runs-on: ubuntu-latest
# needs: [build-check]
# if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch
# permissions:
# contents: read
# packages: write
# steps:
# - uses: actions/checkout@v4
# - uses: dorny/paths-filter@v3
# id: changes
# with:
# filters: |
# build:
# - 'uv.lock'
# - 'Dockerfile'
# - name: Extract project version
# if: steps.changes.outputs.build == 'true'
# id: meta
# run: |
# VERSION=$(sed -n '/^\[project\]/,/^\[/{s/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p}' pyproject.toml | head -n1)
# echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# echo "sha_short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
# echo "Project version: $VERSION"
# - uses: docker/setup-buildx-action@v3
# if: steps.changes.outputs.build == 'true'
# - uses: docker/login-action@v3
# if: steps.changes.outputs.build == 'true'
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Build and push image
# if: steps.changes.outputs.build == 'true'
# uses: docker/build-push-action@v6
# with:
# context: .
# target: dev
# push: true
# build-args: |
# GIT_COMMIT=${{ steps.meta.outputs.sha_short }}
# GIT_BRANCH=${{ github.ref_name }}
# SENTRY_ENVIRONMENT=${{ github.ref_name }}
# SENTRY_RELEASE=${{ steps.meta.outputs.sha_short }}
# tags: |
# ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.sha_short }}
# ghcr.io/${{ github.repository }}:${{ github.ref_name }}-latest
# ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}
# cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
# cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
# provenance: false
# ── Release ───────────────────────────────────────────────────────────────
# NOTE: disabled in template since not all projects will build a Docker image or create formal releases, and we want to keep CI fast by default. Can be enabled as needed.
# promote-image:
# name: Promote Image
# runs-on: ubuntu-latest
# needs: [build-check]
# if: github.event_name == 'push' && (github.ref_name == 'staging' || github.ref_name == 'prod')
# permissions:
# contents: read
# packages: write
# outputs:
# sentry_release: ${{ steps.promote.outputs.sentry_release }}
# sentry_environment: ${{ steps.promote.outputs.sentry_environment }}
# steps:
# - uses: docker/setup-buildx-action@v3
# - uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Promote image
# id: promote
# env:
# IMAGE: ghcr.io/${{ github.repository }}
# run: |
# set -euo pipefail
# SHA_SHORT="${GITHUB_SHA::7}"
# BRANCH="${{ github.ref_name }}"
# if [ "$BRANCH" = "prod" ]; then
# SENTRY_ENVIRONMENT="production"
# else
# SENTRY_ENVIRONMENT="$BRANCH"
# fi
# echo "sentry_release=$SHA_SHORT" >> "$GITHUB_OUTPUT"
# echo "sentry_environment=$SENTRY_ENVIRONMENT" >> "$GITHUB_OUTPUT"
# SRC="$IMAGE:$SHA_SHORT"
# echo "Promoting existing image: $SRC"
# if ! docker manifest inspect "$SRC" >/dev/null 2>&1; then
# echo "❌ No image found for $SRC"
# echo "Ensure a build ran on the default branch for this exact commit first."
# exit 1
# fi
# if [ "$BRANCH" = "staging" ]; then
# T1="$IMAGE:staging-latest"
# T2="$IMAGE:staging-$SHA_SHORT"
# else
# T1="$IMAGE:prod-latest"
# T2="$IMAGE:prod-$SHA_SHORT"
# fi
# echo "Adding tags: $T1 and $T2 -> $SRC"
# docker buildx imagetools create -t "$T1" -t "$T2" "$SRC"
# echo "✅ Promotion complete"
# create-release:
# name: Create Release
# runs-on: ubuntu-latest
# needs: [promote-image]
# continue-on-error: true # mirrors GitLab allow_failure: true
# permissions:
# contents: write
# steps:
# - name: Prepare release metadata
# id: meta
# run: echo "sha_short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
# - name: Create GitHub release
# uses: softprops/action-gh-release@v2
# with:
# tag_name: ${{ steps.meta.outputs.sha_short }}
# name: "${{ steps.meta.outputs.sha_short }} (${{ github.ref_name }})"
# target_commitish: ${{ github.sha }}
# make_latest: ${{ github.ref_name == 'prod' }}
# body: |
# ## Release notes
# - **Branch:** ${{ github.ref_name }}
# - **Commit:** ${{ github.sha }}
# - **Sentry Release:** ${{ needs.promote-image.outputs.sentry_release }}
# - **Sentry Environment:** ${{ needs.promote-image.outputs.sentry_environment }}
# ### Links
# - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
build-and-push-backend-image:
name: Build & Push Backend Image
runs-on: ubuntu-latest
needs: [check-required-files, check-dependencies, lint-and-format, test]
# Allow automatic builds on release branches and manual workflow runs on any ref.
if: >-
(github.event_name == 'push' && contains(fromJson('["main","dev","staging","prod"]'), github.ref_name)) ||
github.event_name == 'workflow_dispatch'
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}/backend
steps:
- uses: actions/checkout@v4
- name: Derive image metadata
id: meta
shell: bash
run: |
short_sha="${GITHUB_SHA::7}"
environment="${GITHUB_REF_NAME}"
sentry_release="backend@${environment}-${short_sha}"
echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT"
echo "environment=${environment}" >> "$GITHUB_OUTPUT"
echo "sentry_release=${sentry_release}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push backend image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
target: prod
push: true
provenance: false
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
build-args: |
GIT_COMMIT=${{ steps.meta.outputs.short_sha }}
GIT_BRANCH=${{ github.ref_name }}
SENTRY_ENVIRONMENT=${{ steps.meta.outputs.environment }}
SENTRY_RELEASE=${{ steps.meta.outputs.sentry_release }}
tags: |
${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.environment }}-${{ steps.meta.outputs.short_sha }}
${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.environment }}-latest
${{ env.IMAGE_NAME }}:sha-${{ steps.meta.outputs.short_sha }}