From 81a1f7817490258ffbedb602f8600c1548dbd6b1 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Fri, 17 Jul 2026 01:42:47 -0300 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20replace=20stacked=20matrix=20with=20l?= =?UTF-8?q?ayered=20L0=E2=80=93L3=20trust=20(uvx/PyPI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror cna-templates layered CI for cpa-templates (#46): - L0 registry + profile validation - L1 every template alone - L2 one extension × fastapi-starter - L3 curated profiles under ci/profiles/ Scaffolding always uses `uvx create-awesome-python-app@latest` (never a create-python-app source checkout). Retire smoke-test.yml and the random/ all-extensions combination workflows. Also split extension categories (ci/containers/database/editor) and fix fastapi-typed-starter mypy issues so L1 type-check is honest. Co-authored-by: Cursor --- .github/workflows/ci-extensions.yml | 78 +++++ .github/workflows/ci-integrity.yml | 35 ++ .github/workflows/ci-profiles.yml | 86 +++++ .github/workflows/ci-templates.yml | 65 ++++ .github/workflows/smoke-test.yml | 86 ----- .github/workflows/test-combinations.yml | 309 ------------------ .gitignore | 1 + README.md | 7 +- ci/profiles/fastapi-default.json | 6 + ci/profiles/fastapi-devcontainer.json | 6 + ci/profiles/fastapi-typed-default.json | 6 + docs/MAINTENANCE_CI.md | 211 ++++-------- docs/MAINTENANCE_DEPENDENCIES.md | 2 +- docs/MAINTENANCE_RUNBOOK.md | 18 +- docs/MAINTENANCE_TEMPLATES.md | 16 +- docs/TESTING.md | 64 ++-- scripts/ci/generate-matrix.py | 216 ++++++++++++ scripts/ci/registry.py | 162 +++++++++ scripts/ci/run-scaffold-check.py | 203 ++++++++++++ scripts/ci/validate-registry.py | 92 ++++++ templates.json | 119 +++++-- .../app/core/exception_handlers.py | 4 +- .../tests/test_health.py.template | 5 +- 23 files changed, 1163 insertions(+), 634 deletions(-) create mode 100644 .github/workflows/ci-extensions.yml create mode 100644 .github/workflows/ci-integrity.yml create mode 100644 .github/workflows/ci-profiles.yml create mode 100644 .github/workflows/ci-templates.yml delete mode 100644 .github/workflows/smoke-test.yml delete mode 100644 .github/workflows/test-combinations.yml create mode 100644 .gitignore create mode 100644 ci/profiles/fastapi-default.json create mode 100644 ci/profiles/fastapi-devcontainer.json create mode 100644 ci/profiles/fastapi-typed-default.json create mode 100755 scripts/ci/generate-matrix.py create mode 100755 scripts/ci/registry.py create mode 100755 scripts/ci/run-scaffold-check.py create mode 100755 scripts/ci/validate-registry.py diff --git a/.github/workflows/ci-extensions.yml b/.github/workflows/ci-extensions.yml new file mode 100644 index 0000000..872dc3e --- /dev/null +++ b/.github/workflows/ci-extensions.yml @@ -0,0 +1,78 @@ +name: CI Extensions (L2) + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + schedule: + - cron: "0 0 * * 0" + workflow_dispatch: + +permissions: + contents: read + +jobs: + generate: + name: Generate extension matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + count: ${{ steps.set.outputs.count }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Generate L2 matrix + id: set + env: + BASE_REF: ${{ github.base_ref }} + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="origin/${BASE_REF}" + git fetch origin "${BASE_REF}" --depth=1 || true + python scripts/ci/generate-matrix.py --layer extensions --changed-only --base-ref "$BASE" + else + python scripts/ci/generate-matrix.py --layer extensions + fi + + extensions: + name: ${{ matrix.cell.id }} + needs: generate + if: needs.generate.outputs.count != '0' + strategy: + fail-fast: false + matrix: + cell: ${{ fromJson(needs.generate.outputs.matrix) }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Scaffold check (one extension via uvx/PyPI) + env: + CI: "true" + CPA_SKIP_GIT: "1" + run: | + ADDON_ARGS=() + for url in ${{ join(matrix.cell.addons, ' ') }}; do + ADDON_ARGS+=(--addon-url "$url") + done + python scripts/ci/run-scaffold-check.py \ + --template-url "${{ matrix.cell.templateUrl }}" \ + --workdir "${{ runner.temp }}/cpa-l2" \ + "${ADDON_ARGS[@]}" diff --git a/.github/workflows/ci-integrity.yml b/.github/workflows/ci-integrity.yml new file mode 100644 index 0000000..8c4c116 --- /dev/null +++ b/.github/workflows/ci-integrity.yml @@ -0,0 +1,35 @@ +name: CI Integrity (L0) + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + push: + branches: [main] + schedule: + - cron: "0 0 * * 0" + workflow_dispatch: + +permissions: + contents: read + +jobs: + integrity: + name: Registry + profiles + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Validate templates.json (on-disk paths + categories) + run: python scripts/ci/validate-registry.py + + - name: Validate curated CI profiles + run: python scripts/ci/generate-matrix.py --layer validate-profiles diff --git a/.github/workflows/ci-profiles.yml b/.github/workflows/ci-profiles.yml new file mode 100644 index 0000000..f8f4d46 --- /dev/null +++ b/.github/workflows/ci-profiles.yml @@ -0,0 +1,86 @@ +name: CI Profiles (L3) + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + schedule: + - cron: "0 0 * * 0" + workflow_dispatch: + +permissions: + contents: read + +jobs: + generate: + name: Generate profile matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + count: ${{ steps.set.outputs.count }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Generate L3 matrix + id: set + env: + BASE_REF: ${{ github.base_ref }} + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="origin/${BASE_REF}" + git fetch origin "${BASE_REF}" --depth=1 || true + python scripts/ci/generate-matrix.py --layer profiles --changed-only --base-ref "$BASE" + else + python scripts/ci/generate-matrix.py --layer profiles + fi + + profiles: + name: ${{ matrix.cell.id }} + needs: generate + if: needs.generate.outputs.count != '0' + strategy: + fail-fast: false + matrix: + cell: ${{ fromJson(needs.generate.outputs.matrix) }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Scaffold check (curated profile via uvx/PyPI) + env: + CI: "true" + CPA_SKIP_GIT: "1" + PROFILE_SETS: ${{ toJson(matrix.cell.sets) }} + run: | + EXTRA=() + while IFS= read -r line; do + [ -n "$line" ] && EXTRA+=(--set "$line") + done < <(python -c 'import json,os; s=json.loads(os.environ.get("PROFILE_SETS") or "{}"); +[print(f"{k}={v}") for k,v in s.items()]') + + ADDON_ARGS=() + for url in ${{ join(matrix.cell.addons, ' ') }}; do + ADDON_ARGS+=(--addon-url "$url") + done + python scripts/ci/run-scaffold-check.py \ + --template-url "${{ matrix.cell.templateUrl }}" \ + --workdir "${{ runner.temp }}/cpa-l3" \ + "${ADDON_ARGS[@]}" \ + "${EXTRA[@]}" diff --git a/.github/workflows/ci-templates.yml b/.github/workflows/ci-templates.yml new file mode 100644 index 0000000..9a902b2 --- /dev/null +++ b/.github/workflows/ci-templates.yml @@ -0,0 +1,65 @@ +name: CI Templates (L1) + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + push: + branches: [main] + schedule: + - cron: "0 0 * * 0" + workflow_dispatch: + +permissions: + contents: read + +jobs: + generate: + name: Generate template matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + count: ${{ steps.set.outputs.count }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Generate L1 matrix + id: set + run: python scripts/ci/generate-matrix.py --layer templates + + templates: + name: ${{ matrix.cell.id }} + needs: generate + if: needs.generate.outputs.count != '0' + strategy: + fail-fast: false + matrix: + cell: ${{ fromJson(needs.generate.outputs.matrix) }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Scaffold check (template alone via uvx/PyPI) + env: + CI: "true" + CPA_SKIP_GIT: "1" + run: | + python scripts/ci/run-scaffold-check.py \ + --template-url "${{ matrix.cell.templateUrl }}" \ + --workdir "${{ runner.temp }}/cpa-l1" diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml deleted file mode 100644 index 8f017dc..0000000 --- a/.github/workflows/smoke-test.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Smoke Test - -on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - workflow_dispatch: - -permissions: - contents: read - -jobs: - smoke-test: - name: Smoke test scaffolded projects - strategy: - fail-fast: false - matrix: - include: - - name: fastapi-starter - extension: "" - - name: fastapi-starter + github-setup - extension: github-setup - - name: fastapi-starter + docker - extension: python-docker - - name: fastapi-starter + postgres - extension: python-postgres - runs-on: ubuntu-latest - steps: - - name: Checkout cpa-templates - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Checkout create-python-app - uses: actions/checkout@v4 - with: - repository: Create-Python-App/create-python-app - path: create-python-app - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Resolve CLI (PyPI preferred, source fallback) - id: cli - run: | - if uv tool install "create-awesome-python-app>=0.1.0"; then - echo "mode=pypi" >> "$GITHUB_OUTPUT" - echo "bin=create-awesome-python-app" >> "$GITHUB_OUTPUT" - else - echo "PyPI install failed; using create-python-app source checkout" - uv sync --directory create-python-app --group dev - echo "mode=source" >> "$GITHUB_OUTPUT" - echo "bin=uv run --directory create-python-app create-awesome-python-app" >> "$GITHUB_OUTPUT" - fi - - - name: Scaffold project - env: - CI: "true" - CPA_SKIP_GIT: "1" - CPA_TEMPLATES_ROOT: ${{ github.workspace }} - run: | - ADDON_ARGS=() - if [ -n "${{ matrix.extension }}" ]; then - ADDON_ARGS+=(--addons "file://${CPA_TEMPLATES_ROOT}?subdir=extensions/${{ matrix.extension }}") - fi - # Typer requires options before the project-directory argument when - # the CLI also exposes subcommands (e.g. `cache`). - ${{ steps.cli.outputs.bin }} \ - --template "file://${CPA_TEMPLATES_ROOT}?subdir=templates/fastapi-starter" \ - --no-interactive --no-install \ - "${ADDON_ARGS[@]}" \ - smoke-test-project - echo "Project scaffolded: ${{ matrix.name }} (cli=${{ steps.cli.outputs.mode }})" - - - name: Run basic checks - working-directory: smoke-test-project - run: | - uv sync - uv run ruff check . - uv run pytest -q - if [ -f compose.yml ]; then test -f compose.yml; fi - if [ -f docker/postgres/compose.yml ]; then test -f docker/postgres/compose.yml; fi - echo "Smoke test passed: ${{ matrix.name }}" diff --git a/.github/workflows/test-combinations.yml b/.github/workflows/test-combinations.yml deleted file mode 100644 index 6d48a6a..0000000 --- a/.github/workflows/test-combinations.yml +++ /dev/null @@ -1,309 +0,0 @@ -name: Test Template and Extension Combinations - -on: - push: - branches: - - main - schedule: - - cron: "0 0 * * 0" # Weekly full matrix on Sunday at midnight UTC - workflow_dispatch: - -permissions: - contents: read - -jobs: - generate-combinations: - name: Generate test combinations - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Generate random combinations - id: set-matrix - run: | - python3 <<'EOF' - import json - import os - import random - import re - - with open("templates.json", encoding="utf-8") as f: - data = json.load(f) - - repo_dir = os.getcwd() - extensions = data["extensions"] - - def ext_local_url(ext): - match = re.search(r"/extensions/([^/]+)(?:/|$)", ext["url"]) - directory = match.group(1) if match else None - return f"file://{repo_dir}?subdir=extensions/{directory}" if directory else None - - def has_incompatibility(selected, candidate): - candidate_slugs = {candidate["slug"]} - selected_slugs = {ext["slug"] for ext in selected} - for ext in selected: - incompatible = ext.get("incompatibleWith") or [] - if any(slug in candidate_slugs for slug in incompatible): - return True - candidate_incompatible = candidate.get("incompatibleWith") or [] - if any(slug in selected_slugs for slug in candidate_incompatible): - return True - return False - - random_combinations = [] - for template in data["templates"]: - compatible_extensions = [ - ext - for ext in extensions - if template["type"] in (ext["type"] if isinstance(ext["type"], list) else [ext["type"]]) - ] - - categories = sorted({ext["category"] for ext in compatible_extensions}) - selected_extensions = [] - for category in categories: - category_extensions = [ext for ext in compatible_extensions if ext["category"] == category] - available = [ext for ext in category_extensions if not has_incompatibility(selected_extensions, ext)] - if not available: - continue - selected_extensions.append(random.choice(available)) - - tpl_match = re.search(r"/templates/([^/]+)(?:/|$)", template["url"]) - tpl_dir = tpl_match.group(1) if tpl_match else None - template_url = ( - f"file://{repo_dir}?subdir=templates/{tpl_dir}" if tpl_dir else template["slug"] - ) - - random_combinations.append( - { - "template": template["slug"], - "templateUrl": template_url, - "extensions": [url for ext in selected_extensions if (url := ext_local_url(ext))], - } - ) - - matrix = json.dumps(random_combinations) - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out: - out.write(f"matrix={matrix}\n") - EOF - - test-combinations: - name: Test ${{ matrix.combination.template }} - needs: generate-combinations - strategy: - fail-fast: false - matrix: - combination: ${{ fromJson(needs.generate-combinations.outputs.matrix) }} - runs-on: ubuntu-latest - steps: - - name: Checkout cpa-templates - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Checkout create-python-app - uses: actions/checkout@v4 - with: - repository: Create-Python-App/create-python-app - path: create-python-app - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Resolve CLI (PyPI preferred, source fallback) - id: cli - run: | - if uv tool install "create-awesome-python-app>=0.1.0"; then - echo "mode=pypi" >> "$GITHUB_OUTPUT" - echo "bin=create-awesome-python-app" >> "$GITHUB_OUTPUT" - else - echo "PyPI install failed; using create-python-app source checkout" - uv sync --directory create-python-app --group dev - echo "mode=source" >> "$GITHUB_OUTPUT" - echo "bin=uv run --directory create-python-app create-awesome-python-app" >> "$GITHUB_OUTPUT" - fi - - - name: Scaffold and test combination - env: - CI: "true" - CPA_SKIP_GIT: "1" - CPA_TEMPLATES_ROOT: ${{ github.workspace }} - run: | - echo "Testing template: ${{ matrix.combination.template }}" - echo "Template URL: ${{ matrix.combination.templateUrl }}" - echo "With extensions: ${{ join(matrix.combination.extensions, ' ') }}" - - ADDON_ARGS=() - ADDONS="${{ join(matrix.combination.extensions, ' ') }}" - if [ -n "$ADDONS" ]; then - read -ra EXT_URLS <<< "$ADDONS" - ADDON_ARGS=(--addons "${EXT_URLS[@]}") - fi - - # Typer requires options before the project-directory argument when - # the CLI also exposes subcommands (e.g. `cache`). - ${{ steps.cli.outputs.bin }} \ - --template "${{ matrix.combination.templateUrl }}" \ - --no-interactive --no-install \ - "${ADDON_ARGS[@]}" \ - combination-test-project - - cd combination-test-project - uv sync - uv run ruff check . - uv run pytest -q - echo "Combination test passed: ${{ matrix.combination.template }} (cli=${{ steps.cli.outputs.mode }})" - - generate-full-matrix: - name: Generate full test matrix - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Generate full combinations matrix - id: set-matrix - run: | - python3 <<'EOF' - import json - import os - import re - - with open("templates.json", encoding="utf-8") as f: - data = json.load(f) - - repo_dir = os.getcwd() - extensions = data["extensions"] - - def ext_local_url(ext): - match = re.search(r"/extensions/([^/]+)(?:/|$)", ext["url"]) - directory = match.group(1) if match else None - return f"file://{repo_dir}?subdir=extensions/{directory}" if directory else None - - def has_incompatibility(selected, candidate): - candidate_slugs = {candidate["slug"]} - selected_slugs = {ext["slug"] for ext in selected} - for ext in selected: - incompatible = ext.get("incompatibleWith") or [] - if any(slug in candidate_slugs for slug in incompatible): - return True - candidate_incompatible = candidate.get("incompatibleWith") or [] - if any(slug in selected_slugs for slug in candidate_incompatible): - return True - return False - - combinations = [] - for template in data["templates"]: - compatible_extensions = [ - ext - for ext in extensions - if template["type"] in (ext["type"] if isinstance(ext["type"], list) else [ext["type"]]) - ] - - selected_extensions = [] - for ext in compatible_extensions: - if not has_incompatibility(selected_extensions, ext): - selected_extensions.append(ext) - - if not selected_extensions: - continue - - tpl_match = re.search(r"/templates/([^/]+)(?:/|$)", template["url"]) - tpl_dir = tpl_match.group(1) if tpl_match else None - template_url = ( - f"file://{repo_dir}?subdir=templates/{tpl_dir}" if tpl_dir else template["slug"] - ) - - combinations.append( - { - "template": template["slug"], - "templateUrl": template_url, - "extensions": [url for ext in selected_extensions if (url := ext_local_url(ext))], - } - ) - - matrix = json.dumps(combinations) - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out: - out.write(f"matrix={matrix}\n") - print(f"Generated {len(combinations)} full combinations") - EOF - - test-full-matrix: - name: Full ${{ matrix.combination.template }} - needs: generate-full-matrix - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - strategy: - fail-fast: false - matrix: - combination: ${{ fromJson(needs.generate-full-matrix.outputs.matrix) }} - runs-on: ubuntu-latest - steps: - - name: Checkout cpa-templates - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Checkout create-python-app - uses: actions/checkout@v4 - with: - repository: Create-Python-App/create-python-app - path: create-python-app - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Resolve CLI (PyPI preferred, source fallback) - id: cli - run: | - if uv tool install "create-awesome-python-app>=0.1.0"; then - echo "mode=pypi" >> "$GITHUB_OUTPUT" - echo "bin=create-awesome-python-app" >> "$GITHUB_OUTPUT" - else - echo "PyPI install failed; using create-python-app source checkout" - uv sync --directory create-python-app --group dev - echo "mode=source" >> "$GITHUB_OUTPUT" - echo "bin=uv run --directory create-python-app create-awesome-python-app" >> "$GITHUB_OUTPUT" - fi - - - name: Scaffold and test full combination - env: - CI: "true" - CPA_SKIP_GIT: "1" - CPA_TEMPLATES_ROOT: ${{ github.workspace }} - run: | - echo "Testing: ${{ matrix.combination.template }}" - echo "With ALL extensions: ${{ join(matrix.combination.extensions, ' ') }}" - - ADDON_ARGS=() - ADDONS="${{ join(matrix.combination.extensions, ' ') }}" - if [ -n "$ADDONS" ]; then - read -ra EXT_URLS <<< "$ADDONS" - ADDON_ARGS=(--addons "${EXT_URLS[@]}") - fi - - ${{ steps.cli.outputs.bin }} \ - --template "${{ matrix.combination.templateUrl }}" \ - --no-interactive --no-install \ - "${ADDON_ARGS[@]}" \ - combination-test-project - - cd combination-test-project - uv sync - uv run ruff check . - uv run pytest -q - echo "Full combination test passed: ${{ matrix.combination.template }} (cli=${{ steps.cli.outputs.mode }})" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03d6e59 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.ci-scaffold/ diff --git a/README.md b/README.md index 12947b3..f6f49e2 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,12 @@ The canonical catalog is [`templates.json`](./templates.json), consumed by the C | Workflow | Trigger | Scope | |----------|---------|-------| -| [Smoke Test](./.github/workflows/smoke-test.yml) | PRs to `main` | Scaffold + basic checks on generated projects | +| [CI Integrity (L0)](./.github/workflows/ci-integrity.yml) | PR + `main` + weekly | Registry paths + curated profiles | +| [CI Templates (L1)](./.github/workflows/ci-templates.yml) | PR + `main` + weekly | Every template alone via `uvx` (PyPI) | +| [CI Extensions (L2)](./.github/workflows/ci-extensions.yml) | PR (changed) + weekly | One extension × canonical template | +| [CI Profiles (L3)](./.github/workflows/ci-profiles.yml) | PR (changed) + weekly | Curated stacks in `ci/profiles/` | + +Scaffolding always uses `uvx create-awesome-python-app@latest` from PyPI (never a source checkout of the CLI). Details: [docs/TESTING.md](./docs/TESTING.md), [#46](https://github.com/Create-Python-App/cpa-templates/issues/46). ## Related repositories diff --git a/ci/profiles/fastapi-default.json b/ci/profiles/fastapi-default.json new file mode 100644 index 0000000..1bb87fe --- /dev/null +++ b/ci/profiles/fastapi-default.json @@ -0,0 +1,6 @@ +{ + "id": "fastapi-default", + "description": "Typical FastAPI API: GitHub CI + Docker + Postgres", + "templateDir": "fastapi-starter", + "addons": ["github-setup", "python-docker", "python-postgres"] +} diff --git a/ci/profiles/fastapi-devcontainer.json b/ci/profiles/fastapi-devcontainer.json new file mode 100644 index 0000000..f509eaf --- /dev/null +++ b/ci/profiles/fastapi-devcontainer.json @@ -0,0 +1,6 @@ +{ + "id": "fastapi-devcontainer", + "description": "FastAPI with GitHub CI and VS Code Dev Container", + "templateDir": "fastapi-starter", + "addons": ["github-setup", "python-devcontainer"] +} diff --git a/ci/profiles/fastapi-typed-default.json b/ci/profiles/fastapi-typed-default.json new file mode 100644 index 0000000..0a490f0 --- /dev/null +++ b/ci/profiles/fastapi-typed-default.json @@ -0,0 +1,6 @@ +{ + "id": "fastapi-typed-default", + "description": "Typed FastAPI API: GitHub CI + Docker + Postgres", + "templateDir": "fastapi-typed-starter", + "addons": ["github-setup", "python-docker", "python-postgres"] +} diff --git a/docs/MAINTENANCE_CI.md b/docs/MAINTENANCE_CI.md index 35b0382..303ff67 100644 --- a/docs/MAINTENANCE_CI.md +++ b/docs/MAINTENANCE_CI.md @@ -8,7 +8,7 @@ ## 1. Workflows overview -### `create-python-app` +### `create-python-app` (CLI monorepo) | Workflow | Purpose | |---|---| @@ -18,205 +18,108 @@ | `mega-linter.yml` | MegaLinter across the repo | | `osv-scanner.yml` | Security scanning | | `publish.yml` | Tag-triggered PyPI release | -| `smoke-distribution.yml` | End-to-end install smoke tests | +| `smoke-distribution.yml` | End-to-end install smoke (`uvx`, Docker, brew, AUR) | | `pr-review.yml` | PR automation | -### `cpa-templates` +### `cpa-templates` (layered trust) | Workflow | Purpose | |---|---| -| `smoke-test.yml` | Quick end-to-end smoke tests on PRs (scaffold + `uv sync` + ruff + pytest) | -| `test-combinations.yml` | Random + full matrix of template × extension combinations | +| `ci-integrity.yml` (L0) | Registry on-disk paths + curated profile validation | +| `ci-templates.yml` (L1) | Every template alone | +| `ci-extensions.yml` (L2) | One extension × canonical template | +| `ci-profiles.yml` (L3) | Curated realistic stacks (`ci/profiles/*.json`) | ---- - -## 2. Reading CI failures - -Always start with the failed logs: +**Hard contract:** scaffolding in `cpa-templates` CI always uses: -```bash -gh run view --repo Create-Python-App/ --log-failed +```sh +uvx create-awesome-python-app@latest … ``` -For long logs, filter: +Never check out `Create-Python-App/create-python-app`. Never fall back to source. Template CI must exercise the same binary users install from PyPI. -```bash -gh run view --repo Create-Python-App/ --log-failed | grep -iE "error|fail|cannot|unable|resolution" -``` +Retired: `smoke-test.yml`, `test-combinations.yml` (random + all-extensions stacks). See [#46](https://github.com/Create-Python-App/cpa-templates/issues/46). -List recent runs: +--- + +## 2. Reading CI failures ```bash -gh run list --repo Create-Python-App/ --limit 20 +gh run view --repo Create-Python-App/cpa-templates --log-failed +gh run list --repo Create-Python-App/cpa-templates --limit 20 ``` +| Red cell pattern | Meaning | +|---|---| +| `L1 ·