diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 776737e41..72463fa13 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -32,30 +32,31 @@ jobs: - name: Check out code uses: actions/checkout@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - name: Install Ruff run: | - python -m pip install --upgrade pip - pip install "ruff==0.9.*" - ruff --version + uvx ruff --version - name: Run Ruff Linting run: | echo "::group::Ruff Linting" - ruff check . --output-format=github + uvx ruff check . --output-format=github echo "::endgroup::" - name: Run Ruff Formatting Check run: | echo "::group::Ruff Formatting" - ruff format --check --diff . + uvx ruff format --check --diff . echo "::endgroup::" test: @@ -71,49 +72,49 @@ jobs: - name: Check out code uses: actions/checkout@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install .[dev] pytest-xdist + uv pip install --system .[dev] "pytest-xdist==3.*" - name: Run tests run: pytest -v -p no:warnings --numprocesses=auto security: name: Security Scan - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: lint steps: - name: Check out code uses: actions/checkout@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - - - name: Install security tools - run: | - python -m pip install --upgrade pip - pip install bandit[toml] - name: Run Bandit security scan run: | # Gate on HIGH severity & MEDIUM confidence; produce JSON artifact - bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q -lll -ii + uvx bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q --severity-level high --confidence-level medium # Human-readable output without affecting job status - bandit -r flixopt/ -c pyproject.toml -q --exit-zero + uvx bandit -r flixopt/ -c pyproject.toml -q --exit-zero - name: Upload security reports uses: actions/upload-artifact@v4 @@ -137,6 +138,12 @@ jobs: with: fetch-depth: 0 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: @@ -166,40 +173,48 @@ jobs: environment: name: testpypi url: https://test.pypi.org/p/flixopt + env: + SKIP_TESTPYPI_UPLOAD: "false" steps: - name: Checkout repository uses: actions/checkout@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install build setuptools wheel twine + uv pip install --system twine - name: Build the distribution run: | - python -m build + uv build - name: Upload to TestPyPI run: | - twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose + twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose --skip-existing env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + TWINE_NON_INTERACTIVE: "1" - name: Test install from TestPyPI + if: env.SKIP_TESTPYPI_UPLOAD != 'true' run: | + set -Eeuo pipefail # Create a temporary environment to test installation - python -m venv test_env + uv venv test_env source test_env/bin/activate + # Get project name from pyproject.toml (PEP 621) PACKAGE_NAME=$(python - <<'PY' import sys, tomllib, pathlib @@ -207,15 +222,18 @@ jobs: print(data["project"]["name"]) PY ) + # Extract version from git tag VERSION=${GITHUB_REF#refs/tags/v} + # Wait and retry while TestPyPI indexes the package INSTALL_SUCCESS=false - for d in 15 30 60 120 180 360 720 1080; do + for d in 10 20 40 80 120; do sleep "$d" echo "Attempting to install $PACKAGE_NAME==$VERSION from TestPyPI (retry after ${d}s)..." + # Install specific version and verify it matches - if pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ + if uv pip --timeout 60 install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then INSTALL_SUCCESS=true break @@ -248,35 +266,40 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install build setuptools wheel twine + uv pip install --system twine - name: Build the distribution run: | - python -m build + uv build - name: Upload to PyPI run: | - twine upload dist/* --verbose + twine upload dist/* --verbose --skip-existing env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + TWINE_NON_INTERACTIVE: "1" - name: Verify PyPI installation run: | + set -Eeuo pipefail # Create a temporary environment to test installation - python -m venv prod_test_env + uv venv prod_test_env source prod_test_env/bin/activate + # Get project name from pyproject.toml (PEP 621) PACKAGE_NAME=$(python - <<'PY' import sys, tomllib, pathlib @@ -284,15 +307,18 @@ jobs: print(data["project"]["name"]) PY ) + # Extract version from git tag VERSION=${GITHUB_REF#refs/tags/v} + # Wait and retry while PyPI indexes the package INSTALL_SUCCESS=false - for d in 5 10 15 30 60 120 180 360 720 1080; do + for d in 10 20 40 80 120; do sleep "$d" echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." + # Install specific version and verify it matches - if pip install "$PACKAGE_NAME==$VERSION" && \ + if uv pip --timeout 60 install "$PACKAGE_NAME==$VERSION" && \ python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then INSTALL_SUCCESS=true break @@ -326,13 +352,16 @@ jobs: with: fetch-depth: 0 # Fetch all history for proper versioning + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.17" + enable-cache: true + - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: | - pyproject.toml - name: Sync changelog to docs run: | @@ -341,8 +370,7 @@ jobs: - name: Install documentation dependencies run: | - python -m pip install --upgrade pip - pip install -e ".[docs]" + uv pip install --system ".[docs]" - name: Configure Git Credentials run: |