ref(config): Migrate from Pydantic to dature for configuration loading #121
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |