diff --git a/.github/workflows/python-cd.yml b/.github/workflows/python-cd.yml new file mode 100644 index 0000000..e5287aa --- /dev/null +++ b/.github/workflows/python-cd.yml @@ -0,0 +1,168 @@ +# Building, testing, and publishing Python releases +# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python CD + +on: + push: + tags: + - 'v*.*.*-*' + +env: + PYTHON_VERSION_FILE: '.python-version' + PACKAGE_NAME: nanotaboada/python-samples-fastapi-restful + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.1 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: version + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + # Extract semver (e.g., v1.0.0-ancelotti -> 1.0.0) + SEMVER=$(echo $TAG_NAME | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)-.*/\1/') + # Extract coach name (e.g., v1.0.0-ancelotti -> ancelotti) + COACH=$(echo $TAG_NAME | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-//') + + # Validate semver format (X.Y.Z) + if ! echo "$SEMVER" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "❌ Error: Invalid semantic version '$SEMVER' extracted from tag '$TAG_NAME'" + echo "Expected format: v{MAJOR}.{MINOR}.{PATCH}-{COACH} (e.g., v1.0.0-ancelotti)" + exit 1 + fi + + # Valid coach names (A-Z from CHANGELOG.md) + VALID_COACHES="ancelotti bielsa capello delbosque eriksson ferguson guardiola heynckes inzaghi klopp kovac low mourinho nagelsmann ottmar pochettino queiroz ranieri simeone tuchel unai vangaal wenger xavi yozhef zeman" + + # Validate coach name against the list + if [ -z "$COACH" ]; then + echo "❌ Error: Coach name is empty in tag '$TAG_NAME'" + echo "Expected format: v{MAJOR}.{MINOR}.{PATCH}-{COACH} (e.g., v1.0.0-ancelotti)" + exit 1 + fi + + if ! echo "$VALID_COACHES" | grep -qw "$COACH"; then + echo "❌ Error: Invalid coach name '$COACH' in tag '$TAG_NAME'" + echo "Valid coaches (A-Z): $VALID_COACHES" + echo "See CHANGELOG.md for the complete list" + exit 1 + fi + + # Export validated outputs + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "semver=$SEMVER" >> $GITHUB_OUTPUT + echo "coach=$COACH" >> $GITHUB_OUTPUT + + echo "📦 Release version: $SEMVER" + echo "♟️ Coach name: $COACH" + + - name: Set up Python + uses: actions/setup-python@v6.1.0 + with: + python-version-file: ${{ env.PYTHON_VERSION_FILE }} + cache: 'pip' + + - name: Install test dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-test.txt + + - name: Run tests with pytest + run: | + pytest -v + + - name: Generate coverage report + run: | + pytest --cov=./ --cov-report=xml --cov-report=term + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.12.0 + + - name: Build and push Docker image to GitHub Container Registry + uses: docker/build-push-action@v6.18.0 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ghcr.io/${{ env.PACKAGE_NAME }}:${{ steps.version.outputs.semver }} + ghcr.io/${{ env.PACKAGE_NAME }}:${{ steps.version.outputs.coach }} + ghcr.io/${{ env.PACKAGE_NAME }}:latest + + - name: Generate changelog + id: changelog + run: | + # Get the previous tag (second most recent tag matching the version pattern) + PREVIOUS_TAG=$(git tag -l 'v*.*.*-*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-' | sed -n '2p') + + if [ -z "$PREVIOUS_TAG" ]; then + echo "📝 First release - no previous tag found" + CHANGELOG="Initial release" + else + echo "📝 Generating changelog from $PREVIOUS_TAG to ${{ steps.version.outputs.tag_name }}" + CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..${{ steps.version.outputs.tag_name }}) + fi + + # Write changelog to file + echo "$CHANGELOG" > changelog.txt + cat changelog.txt + + # Set output for use in release body + { + echo "changelog<> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2.2.0 + with: + name: "v${{ steps.version.outputs.semver }} - ${{ steps.version.outputs.coach }} ♟️" + tag_name: ${{ steps.version.outputs.tag_name }} + body: | + # ♟️ Release ${{ steps.version.outputs.semver }} - ${{ steps.version.outputs.coach }} + + ## Docker Images + + Pull this release using any of the following tags: + + ```bash + # By semantic version (recommended) + docker pull ghcr.io/${{ env.PACKAGE_NAME }}:${{ steps.version.outputs.semver }} + + # By coach name + docker pull ghcr.io/${{ env.PACKAGE_NAME }}:${{ steps.version.outputs.coach }} + + # Latest + docker pull ghcr.io/${{ env.PACKAGE_NAME }}:latest + ``` + + ## Changelog + + ${{ steps.changelog.outputs.changelog }} + + --- + + 📦 **Package:** [ghcr.io/${{ env.PACKAGE_NAME }}](https://github.com/${{ github.repository }}/pkgs/container/python-samples-fastapi-restful) + draft: false + prerelease: false + generate_release_notes: true diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-ci.yml similarity index 68% rename from .github/workflows/python-app.yml rename to .github/workflows/python-ci.yml index bb8e87f..db3a57d 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-ci.yml @@ -16,13 +16,13 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.1 - name: Lint commit messages uses: wagoid/commitlint-github-action@v6.2.1 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v6.1.0 with: python-version-file: '.python-version' cache: 'pip' @@ -47,10 +47,10 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.1 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v6.1.0 with: python-version-file: '.python-version' cache: 'pip' @@ -87,7 +87,7 @@ jobs: service: [codecov, codacy] steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.1 - name: Download coverage report artifact uses: actions/download-artifact@v7.0.0 @@ -107,39 +107,3 @@ jobs: with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} coverage-reports: coverage.xml - - container: - needs: coverage - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.12.0 - - - name: Build and push Docker image to GitHub Container Registry - uses: docker/build-push-action@v6.18.0 - with: - context: . - push: true - platforms: linux/amd64 - provenance: false - cache-from: type=gha - cache-to: type=gha,mode=max - tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:sha-${{ github.sha }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c8eff6f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Release Naming Convention ♟️ + +This project uses famous football coaches as release codenames, following an A-Z naming pattern. + +| Letter | Coach Name | Country/Notable Era | Tag Name | +|--------|-----------|---------------------|----------| +| A | Ancelotti (Carlo) | Italy | `ancelotti` | +| B | Bielsa (Marcelo) | Argentina | `bielsa` | +| C | Capello (Fabio) | Italy | `capello` | +| D | Del Bosque (Vicente) | Spain | `delbosque` | +| E | Eriksson (Sven-Göran) | Sweden | `eriksson` | +| F | Ferguson (Alex) | Scotland | `ferguson` | +| G | Guardiola (Pep) | Spain | `guardiola` | +| H | Heynckes (Jupp) | Germany | `heynckes` | +| I | Inzaghi (Simone) | Italy | `inzaghi` | +| J | Klopp (Jürgen) | Germany | `klopp` | +| K | Kovač (Niko) | Croatia | `kovac` | +| L | Löw (Joachim) | Germany | `low` | +| M | Mourinho (José) | Portugal | `mourinho` | +| N | Nagelsmann (Julian) | Germany | `nagelsmann` | +| O | Ottmar Hitzfeld | Germany/Switzerland | `ottmar` | +| P | Pochettino (Mauricio) | Argentina | `pochettino` | +| Q | Queiroz (Carlos) | Portugal | `queiroz` | +| R | Ranieri (Claudio) | Italy | `ranieri` | +| S | Simeone (Diego) | Argentina | `simeone` | +| T | Tuchel (Thomas) | Germany | `tuchel` | +| U | Unai Emery | Spain | `unai` | +| V | Van Gaal (Louis) | Netherlands | `vangaal` | +| W | Wenger (Arsène) | France | `wenger` | +| X | Xavi Hernández | Spain | `xavi` | +| Y | Yozhef Sabo | Ukraine | `yozhef` | +| Z | Zeman (Zdeněk) | Czech Republic | `zeman` | + +--- + +## [Unreleased] + +### Added + +### Changed + +### Fixed + +### Removed + +--- + +## [1.0.0 - Ancelotti] - TBD + +### Added + +- Initial stable release +- Player CRUD operations with FastAPI +- SQLite database with SQLAlchemy (async) +- Docker support with Docker Compose +- In-memory caching with aiocache (10 min TTL) +- Comprehensive pytest test suite with coverage reporting +- Health check endpoint +- CI/CD pipeline with tag-based releases +- Famous coaches release naming convention ♟️ + +### Changed + +- N/A + +### Fixed + +- N/A + +### Removed + +- N/A + +--- + +## How to Release + +1. Update this CHANGELOG.md with the new version details +2. Create a tag with the format `v{MAJOR}.{MINOR}.{PATCH}-{COACH}` +3. Push the tag to trigger the CD pipeline + +```bash +# Example: Creating the first release (Ancelotti) +git tag -a v1.0.0-ancelotti -m "Release 1.0.0 - Ancelotti" +git push origin v1.0.0-ancelotti +``` + +The CD pipeline will automatically: + +- Run tests and generate coverage reports +- Build and push Docker images with multiple tags (`:1.0.0`, `:ancelotti`, `:latest`) +- Generate a changelog from git commits +- Create a GitHub Release with auto-generated notes + +## Version History + + diff --git a/README.md b/README.md index 2f8f1a0..402dc3a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Status -[![Python CI](https://github.com/nanotaboada/python-samples-fastapi-restful/actions/workflows/python-app.yml/badge.svg)](https://github.com/nanotaboada/python-samples-fastapi-restful/actions/workflows/python-app.yml) +[![Python CI](https://github.com/nanotaboada/python-samples-fastapi-restful/actions/workflows/python-ci.yml/badge.svg)](https://github.com/nanotaboada/python-samples-fastapi-restful/actions/workflows/python-ci.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nanotaboada_python-samples-fastapi-restful&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nanotaboada_python-samples-fastapi-restful) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/8f9bab37f6f444c895a8b25d5df772fc)](https://app.codacy.com/gh/nanotaboada/python-samples-fastapi-restful/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![codecov](https://codecov.io/gh/nanotaboada/python-samples-fastapi-restful/branch/master/graph/badge.svg?token=A1WNZPRQEJ)](https://codecov.io/gh/nanotaboada/python-samples-fastapi-restful) @@ -82,6 +82,62 @@ docker compose down -v > This removes the volume and will reinitialize the database from the built-in seed file the next time you `up`. +## Releases + +This project uses famous football coaches as release names ♟️ + +### Create a Release + +Releases are created by pushing version tags in the format `v{MAJOR}.{MINOR}.{PATCH}-{COACH}`: + +```bash +# Example: Creating the first release (Ancelotti) +git tag -a v1.0.0-ancelotti -m "Release 1.0.0 - Ancelotti" +git push origin v1.0.0-ancelotti +``` + +The CD pipeline will automatically: + +- Run tests and generate coverage reports +- Build and push Docker images with multiple tags +- Generate a changelog from git commits +- Create a GitHub Release with auto-generated notes + +### Pull Docker Images + +Official releases are published to GitHub Container Registry (GHCR): + +```bash +# By semantic version (recommended) +docker pull ghcr.io/nanotaboada/python-samples-fastapi-restful:1.0.0 + +# By coach name +docker pull ghcr.io/nanotaboada/python-samples-fastapi-restful:ancelotti + +# Latest +docker pull ghcr.io/nanotaboada/python-samples-fastapi-restful:latest +``` + +### Coach Names (A-Z) + +| Letter | Coach | Tag Name | Letter | Coach | Tag Name | +|--------|-------|----------|--------|-------|----------| +| A | Ancelotti | `ancelotti` | N | Nagelsmann | `nagelsmann` | +| B | Bielsa | `bielsa` | O | Ottmar | `ottmar` | +| C | Capello | `capello` | P | Pochettino | `pochettino` | +| D | Del Bosque | `delbosque` | Q | Queiroz | `queiroz` | +| E | Eriksson | `eriksson` | R | Ranieri | `ranieri` | +| F | Ferguson | `ferguson` | S | Simeone | `simeone` | +| G | Guardiola | `guardiola` | T | Tuchel | `tuchel` | +| H | Heynckes | `heynckes` | U | Unai | `unai` | +| I | Inzaghi | `inzaghi` | V | Van Gaal | `vangaal` | +| J | Klopp | `klopp` | W | Wenger | `wenger` | +| K | Kovač | `kovac` | X | Xavi | `xavi` | +| L | Löw | `low` | Y | Yozhef | `yozhef` | +| M | Mourinho | `mourinho` | Z | Zeman | `zeman` | + +See [CHANGELOG.md](CHANGELOG.md) for full release history. + ## Credits The solution has been coded using [Visual Studio Code](https://code.visualstudio.com/) with the official [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) extension.