From bcab67145f2e10b5a16c51ec0aa26c5c1a4de80d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:39:35 +0200 Subject: [PATCH 01/26] Improve CI, add formating, security scan & multi-os-test --- .github/workflows/python-app.yaml | 340 +++++++++++++++++++++--------- pyproject.toml | 4 + 2 files changed, 243 insertions(+), 101 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index a60a2959a..1cac0e4d1 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -2,26 +2,55 @@ name: Python Package CI/CD on: push: - branches: [main] # Run on pushes to main (after PR is merged) + branches: [main, dev] # Added develop branch tags: - - 'v*' # Trigger on version tags + - 'v*' pull_request: - branches: [main, next/*] # Run when PRs are created or updated + branches: [main, dev, 'feature/*', 'hotfix/*'] types: [opened, synchronize, reopened] release: - types: [created] # Trigger when a release is created + types: [created] + workflow_dispatch: # Allow manual triggering + +# Set permissions for security +permissions: + contents: read + +# Cancel previous runs on new push +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.11" jobs: - lint: - runs-on: ubuntu-22.04 + code-quality: + name: Code Quality (Ruff) + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + # Cache pip dependencies + - name: Cache pip dependencies + uses: actions/cache@v4 with: - python-version: "3.11" # Ruff tasks can run with a single Python version + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Generate cache key + id: cache-key + run: echo "key=${{ runner.os }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/pyproject.toml') }}" >> $GITHUB_OUTPUT - name: Install Ruff run: | @@ -29,38 +58,106 @@ jobs: pip install ruff - name: Run Ruff Linting - run: ruff check . + run: | + echo "::group::Ruff Linting" + ruff check . --output-format=github + echo "::endgroup::" + + - name: Run Ruff Formatting Check + run: | + echo "::group::Ruff Formatting" + ruff format --check --diff . + echo "::endgroup::" test: - runs-on: ubuntu-22.04 - needs: lint # Run tests only after linting passes + name: Test (Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + needs: code-quality strategy: - fail-fast: false # Continue testing other Python versions if one fails + fail-fast: false matrix: + os: [ubuntu-latest] python-version: ['3.10', '3.11', '3.12', '3.13'] + include: + # Test on multiple OS for latest Python + - os: macos-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.11' steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + .pytest_cache + key: ${{ needs.code-quality.outputs.cache-key }}-${{ matrix.python-version }} + restore-keys: | + ${{ runner.os }}-python-${{ matrix.python-version }}- + - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[dev] - - name: Run tests - run: pytest -v -p no:warnings + - name: Run tests with coverage + run: | + pytest -v --cov=src --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + if: matrix.python-version == env.PYTHON_VERSION && matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + + security: + name: Security Scan + runs-on: ubuntu-latest + needs: code-quality + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install security tools + run: | + python -m pip install --upgrade pip + pip install bandit[toml] safety + + - name: Run Bandit security scan + run: | + bandit -r src/ -f json -o bandit-report.json || true + bandit -r src/ + + - name: Upload security reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-report + path: | + bandit-report.json create-release: - name: Create Release with Changelog - runs-on: ubuntu-22.04 - needs: [test] + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [test, security] if: startsWith(github.ref, 'refs/tags/v') + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout repository @@ -69,9 +166,21 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Extract version and validate tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Validate semantic versioning + if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "Invalid version format: $VERSION" + exit 1 + fi - name: Sync changelog to docs run: | @@ -80,144 +189,173 @@ jobs: - name: Extract release notes run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "Extracting release notes for version: $VERSION" - python scripts/extract_release_notes.py $VERSION > current_release_notes.md + echo "Extracting release notes for version: ${{ steps.version.outputs.version }}" + python scripts/extract_release_notes.py ${{ steps.version.outputs.version }} > current_release_notes.md - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + id: create_release + uses: softprops/action-gh-release@v2 with: body_path: current_release_notes.md draft: false - prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} + prerelease: ${{ contains(steps.version.outputs.version, 'alpha') || contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'rc') }} + generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - publish-testpypi: - name: Publish to TestPyPI - runs-on: ubuntu-22.04 - needs: [test, create-release] # Run after tests and release creation - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push + build: + name: Build Distribution + runs-on: ubuntu-latest + needs: [test, security] + if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - - name: Install dependencies + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install build setuptools wheel twine + pip install build twine - - name: Build the distribution - run: | - python -m build + - name: Build distribution + run: python -m build - - name: Upload to TestPyPI - run: | - twine upload --repository testpypi dist/* --verbose - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} - - - name: Test install from TestPyPI - run: | - # Create a temporary environment to test installation - python -m venv test_env - source test_env/bin/activate - # Get the package name from the built distribution - PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') - # Install from TestPyPI with retry (TestPyPI can be slow to index) - pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME || \ - (sleep 30 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) || \ - (sleep 60 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) - # Basic import test - python -c "import flixopt; print('Installation successful!')" + - name: Check distribution + run: twine check dist/* - publish-pypi: - name: Publish to PyPI - runs-on: ubuntu-22.04 - needs: [publish-testpypi] # Only run after TestPyPI publish succeeds - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: distribution-files + path: dist/ + retention-days: 7 + + publish-testpypi: + name: Publish to TestPyPI + runs-on: ubuntu-latest + needs: [create-release, build] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + environment: + name: testpypi + url: https://test.pypi.org/p/flixopt steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: distribution-files + path: dist/ - - name: Set up Python - uses: actions/setup-python@v4 + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 with: - python-version: "3.11" + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + verbose: true - - name: Install dependencies + - name: Test TestPyPI installation run: | - python -m pip install --upgrade pip - pip install build setuptools wheel twine + pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ flixopt + python -c "import flixopt; print('TestPyPI installation successful!')" - - name: Build the distribution - run: | - python -m build + publish-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [publish-testpypi] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + environment: + name: pypi + url: https://pypi.org/p/flixopt - - name: Upload to PyPI - run: | - twine upload dist/* --verbose - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: distribution-files + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true - name: Verify PyPI installation run: | - # Create a temporary environment to test installation - python -m venv prod_test_env - source prod_test_env/bin/activate - # Get the package name from the built distribution - PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') - # Wait for PyPI to index the package - sleep 60 - # Install from PyPI - pip install $PACKAGE_NAME - # Basic import test + sleep 60 # Wait for PyPI indexing + pip install flixopt python -c "import flixopt; print('PyPI installation successful!')" deploy-docs: name: Deploy Documentation - runs-on: ubuntu-22.04 - needs: [publish-pypi] # Deploy docs after successful PyPI publishing + runs-on: ubuntu-latest + needs: [publish-pypi] if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, 'rc') + environment: + name: docs + url: https://your-docs-url.com steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch all history for proper versioning + fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - - name: Sync changelog to docs - run: | - cp CHANGELOG.md docs/changelog.md - echo "✅ Synced changelog to docs" + - name: Cache documentation dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-docs-${{ hashFiles('**/pyproject.toml') }} - name: Install documentation dependencies run: | python -m pip install --upgrade pip pip install -e ".[docs]" - - name: Configure Git Credentials + - name: Sync changelog to docs + run: | + cp CHANGELOG.md docs/changelog.md + echo "✅ Synced changelog to docs" + + - name: Configure Git run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - name: Deploy docs + - name: Deploy documentation run: | VERSION=${GITHUB_REF#refs/tags/v} - echo "Deploying docs after successful PyPI publish: $VERSION" + echo "Deploying docs for version: $VERSION" mike deploy --push --update-aliases $VERSION latest mike set-default --push latest + + notify: + name: Notify on Completion + runs-on: ubuntu-latest + needs: [deploy-docs] + if: always() && startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Notify success + if: needs.deploy-docs.result == 'success' + run: | + echo "🎉 Release pipeline completed successfully!" + echo "✅ Published to PyPI" + echo "✅ Documentation deployed" + + - name: Notify failure + if: failure() + run: | + echo "❌ Release pipeline failed!" + echo "Check the logs for details." \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 99a1d061d..389d0ef4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,3 +150,7 @@ extend-fixable = ["B"] # Enable fix for flake8-bugbear (`B`), on top of any ru quote-style = "single" indent-style = "space" docstring-code-format = true + +[tool.bandit] +skips = ["B101", "B506"] # assert_used and yaml_load +exclude_dirs = ["tests/"] From 2df7206a7bfab780cd6a8068d59f4882bc4c3a0b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:05:43 +0200 Subject: [PATCH 02/26] Update CI --- .github/workflows/python-app.yaml | 49 +++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 1cac0e4d1..62de6f353 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -111,7 +111,7 @@ jobs: - name: Run tests with coverage run: | - pytest -v --cov=src --cov-report=xml --cov-report=term-missing + pytest -v --cov=flixopt --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov if: matrix.python-version == env.PYTHON_VERSION && matrix.os == 'ubuntu-latest' @@ -120,6 +120,45 @@ jobs: file: ./coverage.xml fail_ci_if_error: false + test-release: + name: Release Test (${{ matrix.os }} Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + needs: code-quality + if: startsWith(github.ref, 'refs/tags/v') # Only run on version tags + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + python-version: ['3.11'] + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + .pytest_cache + key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-python-${{ matrix.python-version }}- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + + - name: Run tests with coverage + run: | + pytest -v --cov=flixopt --cov-report=xml --cov-report=term-missing + security: name: Security Scan runs-on: ubuntu-latest @@ -136,12 +175,12 @@ jobs: - name: Install security tools run: | python -m pip install --upgrade pip - pip install bandit[toml] safety + pip install bandit[toml] - name: Run Bandit security scan run: | - bandit -r src/ -f json -o bandit-report.json || true - bandit -r src/ + bandit -r flixopt/ -f json -o bandit-report.json || true + bandit -r flixopt/ - name: Upload security reports uses: actions/upload-artifact@v4 @@ -154,7 +193,7 @@ jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [test, security] + needs: [test, test-release, security] if: startsWith(github.ref, 'refs/tags/v') outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} From 85caa71dd4625ba50a8663af45330ecfa3685e7d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:17:10 +0200 Subject: [PATCH 03/26] Revert some CI Updates --- .github/workflows/python-app.yaml | 291 ++++++++++-------------------- 1 file changed, 92 insertions(+), 199 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 62de6f353..5ddcc2554 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -4,12 +4,12 @@ on: push: branches: [main, dev] # Added develop branch tags: - - 'v*' + - 'v*' # Trigger on version tags pull_request: - branches: [main, dev, 'feature/*', 'hotfix/*'] + branches: [main, dev, 'feature/*', 'hotfix/*', 'fix/*'] types: [opened, synchronize, reopened] release: - types: [created] + types: [created] # Trigger when a release is created workflow_dispatch: # Allow manual triggering # Set permissions for security @@ -25,11 +25,8 @@ env: PYTHON_VERSION: "3.11" jobs: - code-quality: - name: Code Quality (Ruff) - runs-on: ubuntu-latest - outputs: - cache-key: ${{ steps.cache-key.outputs.key }} + lint: + runs-on: ubuntu-22.04 steps: - name: Check out code uses: actions/checkout@v4 @@ -39,19 +36,6 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - # Cache pip dependencies - - name: Cache pip dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Generate cache key - id: cache-key - run: echo "key=${{ runner.os }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/pyproject.toml') }}" >> $GITHUB_OUTPUT - - name: Install Ruff run: | python -m pip install --upgrade pip @@ -70,66 +54,12 @@ jobs: echo "::endgroup::" test: - name: Test (Python ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - needs: code-quality + runs-on: ubuntu-22.04 + needs: lint # Run tests only after linting passes strategy: - fail-fast: false + fail-fast: false # Continue testing other Python versions if one fails matrix: - os: [ubuntu-latest] python-version: ['3.10', '3.11', '3.12', '3.13'] - include: - # Test on multiple OS for latest Python - - os: macos-latest - python-version: '3.11' - - os: windows-latest - python-version: '3.11' - - steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cache/pip - .pytest_cache - key: ${{ needs.code-quality.outputs.cache-key }}-${{ matrix.python-version }} - restore-keys: | - ${{ runner.os }}-python-${{ matrix.python-version }}- - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] - - - name: Run tests with coverage - run: | - pytest -v --cov=flixopt --cov-report=xml --cov-report=term-missing - - - name: Upload coverage to Codecov - if: matrix.python-version == env.PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - fail_ci_if_error: false - - test-release: - name: Release Test (${{ matrix.os }} Python ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - needs: code-quality - if: startsWith(github.ref, 'refs/tags/v') # Only run on version tags - strategy: - fail-fast: false - matrix: - os: [macos-latest, windows-latest] - python-version: ['3.11'] steps: - name: Check out code @@ -155,14 +85,13 @@ jobs: python -m pip install --upgrade pip pip install .[dev] - - name: Run tests with coverage - run: | - pytest -v --cov=flixopt --cov-report=xml --cov-report=term-missing + - name: Run tests + run: pytest -v -p no:warnings security: name: Security Scan runs-on: ubuntu-latest - needs: code-quality + needs: lint steps: - name: Check out code uses: actions/checkout@v4 @@ -193,10 +122,8 @@ jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [test, test-release, security] + needs: [lint, test, security] if: startsWith(github.ref, 'refs/tags/v') - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout repository @@ -209,18 +136,6 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Extract version and validate tag - id: version - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - # Validate semantic versioning - if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then - echo "Invalid version format: $VERSION" - exit 1 - fi - - name: Sync changelog to docs run: | cp CHANGELOG.md docs/changelog.md @@ -228,25 +143,28 @@ jobs: - name: Extract release notes run: | - echo "Extracting release notes for version: ${{ steps.version.outputs.version }}" - python scripts/extract_release_notes.py ${{ steps.version.outputs.version }} > current_release_notes.md + VERSION=${GITHUB_REF#refs/tags/v} + echo "Extracting release notes for version: $VERSION" + python scripts/extract_release_notes.py $VERSION > current_release_notes.md - name: Create GitHub Release - id: create_release uses: softprops/action-gh-release@v2 with: body_path: current_release_notes.md draft: false - prerelease: ${{ contains(steps.version.outputs.version, 'alpha') || contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'rc') }} + prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: - name: Build Distribution - runs-on: ubuntu-latest - needs: [test, security] - if: startsWith(github.ref, 'refs/tags/v') + publish-testpypi: + name: Publish to TestPyPI + runs-on: ubuntu-22.04 + needs: [test, create-release] # Run after tests and release creation + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push + environment: + name: testpypi + url: https://test.pypi.org/p/flixopt steps: - name: Checkout repository @@ -257,144 +175,119 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Install build dependencies + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build twine - - - name: Build distribution - run: python -m build + pip install build setuptools wheel twine - - name: Check distribution - run: twine check dist/* - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: distribution-files - path: dist/ - retention-days: 7 - - publish-testpypi: - name: Publish to TestPyPI - runs-on: ubuntu-latest - needs: [create-release, build] - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - environment: - name: testpypi - url: https://test.pypi.org/p/flixopt - - steps: - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: distribution-files - path: dist/ + - name: Build the distribution + run: | + python -m build - - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - verbose: true + - name: Upload to TestPyPI + run: | + twine upload --repository testpypi dist/* --verbose + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} - - name: Test TestPyPI installation + - name: Test install from TestPyPI run: | - pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ flixopt - python -c "import flixopt; print('TestPyPI installation successful!')" + # Create a temporary environment to test installation + python -m venv test_env + source test_env/bin/activate + # Get the package name from the built distribution + PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') + # Install from TestPyPI with retry (TestPyPI can be slow to index) + pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME || \ + (sleep 30 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) || \ + (sleep 60 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) + # Basic import test + python -c "import flixopt; print('Installation successful!')" publish-pypi: name: Publish to PyPI - runs-on: ubuntu-latest - needs: [publish-testpypi] - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-22.04 + needs: [publish-testpypi] # Only run after TestPyPI publish succeeds + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push environment: name: pypi url: https://pypi.org/p/flixopt steps: - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: distribution-files - path: dist/ + - name: Checkout repository + uses: actions/checkout@v4 - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Set up Python + uses: actions/setup-python@v5 with: - password: ${{ secrets.PYPI_API_TOKEN }} - verbose: true + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build setuptools wheel twine + + - name: Build the distribution + run: | + python -m build + + - name: Upload to PyPI + run: | + twine upload dist/* --verbose + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - name: Verify PyPI installation run: | - sleep 60 # Wait for PyPI indexing - pip install flixopt + # Create a temporary environment to test installation + python -m venv prod_test_env + source prod_test_env/bin/activate + # Get the package name from the built distribution + PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') + # Wait for PyPI to index the package + sleep 60 + # Install from PyPI + pip install $PACKAGE_NAME + # Basic import test python -c "import flixopt; print('PyPI installation successful!')" deploy-docs: name: Deploy Documentation - runs-on: ubuntu-latest - needs: [publish-pypi] + runs-on: ubuntu-22.04 + needs: [publish-pypi] # Deploy docs after successful PyPI publishing if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, 'rc') - environment: - name: docs - url: https://your-docs-url.com steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 # Fetch all history for proper versioning - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Cache documentation dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-docs-${{ hashFiles('**/pyproject.toml') }} + - name: Sync changelog to docs + run: | + cp CHANGELOG.md docs/changelog.md + echo "✅ Synced changelog to docs" - name: Install documentation dependencies run: | python -m pip install --upgrade pip pip install -e ".[docs]" - - name: Sync changelog to docs - run: | - cp CHANGELOG.md docs/changelog.md - echo "✅ Synced changelog to docs" - - - name: Configure Git + - name: Configure Git Credentials run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - name: Deploy documentation + - name: Deploy docs run: | VERSION=${GITHUB_REF#refs/tags/v} - echo "Deploying docs for version: $VERSION" + echo "Deploying docs after successful PyPI publish: $VERSION" mike deploy --push --update-aliases $VERSION latest - mike set-default --push latest - - notify: - name: Notify on Completion - runs-on: ubuntu-latest - needs: [deploy-docs] - if: always() && startsWith(github.ref, 'refs/tags/v') - - steps: - - name: Notify success - if: needs.deploy-docs.result == 'success' - run: | - echo "🎉 Release pipeline completed successfully!" - echo "✅ Published to PyPI" - echo "✅ Documentation deployed" - - - name: Notify failure - if: failure() - run: | - echo "❌ Release pipeline failed!" - echo "Check the logs for details." \ No newline at end of file + mike set-default --push latest \ No newline at end of file From f05eda581568e5a3e200a77585ccd4d35598259f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:29:40 +0200 Subject: [PATCH 04/26] Fix bandit --- .github/workflows/python-app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 5ddcc2554..8e2ab09a8 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -108,8 +108,8 @@ jobs: - name: Run Bandit security scan run: | - bandit -r flixopt/ -f json -o bandit-report.json || true - bandit -r flixopt/ + bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json || true + bandit -r flixopt/ -c pyproject.toml - name: Upload security reports uses: actions/upload-artifact@v4 From c5fe17318985881c40ea7bc3f13e3122f245fa72 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:35:29 +0200 Subject: [PATCH 05/26] Add parallelized pytest --- .github/workflows/python-app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 8e2ab09a8..e362088a9 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -83,10 +83,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[dev] + pip install .[dev] pytest-xdist - name: Run tests - run: pytest -v -p no:warnings + run: pytest -v -p no:warnings --numprocesses=auto security: name: Security Scan From 15edb5a59297e1962e4e1d30940f2d28eb3ae835 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:38:33 +0200 Subject: [PATCH 06/26] Improve bandit --- .github/workflows/python-app.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index e362088a9..fdb347d68 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -108,8 +108,10 @@ jobs: - name: Run Bandit security scan run: | - bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json || true - bandit -r flixopt/ -c pyproject.toml + # Gate on HIGH severity & HIGH confidence; produce JSON artifact + bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q -lll -ii + # Human-readable output without affecting job status + bandit -r flixopt/ -c pyproject.toml -q --exit-zero - name: Upload security reports uses: actions/upload-artifact@v4 From f25942b9a3ff596c3bdf271ca02c6fd35645487f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:40:51 +0200 Subject: [PATCH 07/26] Add caching --- .github/workflows/python-app.yaml | 51 +++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index fdb347d68..0b2c0ad07 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -36,6 +36,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Cache pip dependencies for linting tools + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-lint-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-lint-pip- + - name: Install Ruff run: | python -m pip install --upgrade pip @@ -70,15 +79,17 @@ jobs: with: python-version: ${{ matrix.python-version }} + # Cache pip and pytest for each Python version - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.cache/pip .pytest_cache - key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + key: ${{ runner.os }}-test-py${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} restore-keys: | - ${{ runner.os }}-python-${{ matrix.python-version }}- + ${{ runner.os }}-test-py${{ matrix.python-version }}- + ${{ runner.os }}-test- - name: Install dependencies run: | @@ -101,6 +112,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Cache security tools + - name: Cache security tools + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-security-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-security-pip- + - name: Install security tools run: | python -m pip install --upgrade pip @@ -177,6 +197,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Cache build tools + - name: Cache build tools + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-build-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-build-pip- + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -225,6 +254,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Cache build tools (can reuse from TestPyPI job if same runner) + - name: Cache build tools + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-build-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-build-pip- + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -272,6 +310,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + # Cache documentation dependencies + - name: Cache documentation dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-docs-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-docs-pip- + - name: Sync changelog to docs run: | cp CHANGELOG.md docs/changelog.md From cc3d961cb7f2201c1eca257a1d3a964f02a6d8b8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:41:51 +0200 Subject: [PATCH 08/26] Adjust job permissions --- .github/workflows/python-app.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 0b2c0ad07..effefb03d 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -144,6 +144,8 @@ jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest + permissions: + contents: write needs: [lint, test, security] if: startsWith(github.ref, 'refs/tags/v') @@ -296,6 +298,8 @@ jobs: deploy-docs: name: Deploy Documentation runs-on: ubuntu-22.04 + permissions: + contents: write needs: [publish-pypi] # Deploy docs after successful PyPI publishing if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, 'rc') From 1d719ce4376afd78d50c85d47453e984e6790aa2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:15:09 +0200 Subject: [PATCH 09/26] FIx path creation issue --- flixopt/calculation.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c7367cad2..32bd3ce7e 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -62,13 +62,12 @@ def __init__( self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) self.results: Optional[CalculationResults] = None - if not self.folder.exists(): - try: - self.folder.mkdir(parents=False) - except FileNotFoundError as e: - raise FileNotFoundError( - f'Folder {self.folder} and its parent do not exist. Please create them first.' - ) from e + try: + self.folder.mkdir(parents=False, exist_ok=True) + except FileNotFoundError as e: + raise FileNotFoundError( + f'Folder {self.folder} and its parent do not exist. Please create them first.' + ) from e @property def main_results(self) -> Dict[str, Union[Scalar, Dict]]: From d955b3b26b219fdfe5a3c03630ede05ee16004f3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:18:59 +0200 Subject: [PATCH 10/26] Fix Comment --- .github/workflows/python-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index effefb03d..663d5f177 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -128,7 +128,7 @@ jobs: - name: Run Bandit security scan run: | - # Gate on HIGH severity & HIGH confidence; produce JSON artifact + # Gate on HIGH severity & MEDIUM confidence; produce JSON artifact bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q -lll -ii # Human-readable output without affecting job status bandit -r flixopt/ -c pyproject.toml -q --exit-zero From a2ef7f332a66e8c3fb111f458699f46c3c720292 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:20:11 +0200 Subject: [PATCH 11/26] Fix workflow double run --- .github/workflows/python-app.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 663d5f177..3f538dcfb 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -4,12 +4,10 @@ on: push: branches: [main, dev] # Added develop branch tags: - - 'v*' # Trigger on version tags + - 'v*.*.*' # Trigger on semantic version tags pull_request: branches: [main, dev, 'feature/*', 'hotfix/*', 'fix/*'] types: [opened, synchronize, reopened] - release: - types: [created] # Trigger when a release is created workflow_dispatch: # Allow manual triggering # Set permissions for security From eb069fc7899eef47bb5ff778976b300ef4e99822 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:25:16 +0200 Subject: [PATCH 12/26] Change source of package name --- .github/workflows/python-app.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 3f538dcfb..e733c4e73 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -227,8 +227,13 @@ jobs: # Create a temporary environment to test installation python -m venv test_env source test_env/bin/activate - # Get the package name from the built distribution - PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') + # Get project name from pyproject.toml (PEP 621) + PACKAGE_NAME=$(python - <<'PY' + import sys, tomllib, pathlib + data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) + print(data["project"]["name"]) + PY + ) # Install from TestPyPI with retry (TestPyPI can be slow to index) pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME || \ (sleep 30 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) || \ @@ -284,8 +289,13 @@ jobs: # Create a temporary environment to test installation python -m venv prod_test_env source prod_test_env/bin/activate - # Get the package name from the built distribution - PACKAGE_NAME=$(ls dist/*.tar.gz | head -n 1 | sed 's/dist\///' | sed 's/-[0-9].*$//') + # Get project name from pyproject.toml (PEP 621) + PACKAGE_NAME=$(python - <<'PY' + import sys, tomllib, pathlib + data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) + print(data["project"]["name"]) + PY + ) # Wait for PyPI to index the package sleep 60 # Install from PyPI From 81b55bd715813b32a2d1583cbd72d279fc0eddc7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:29:40 +0200 Subject: [PATCH 13/26] Make import test reliably get the new version --- .github/workflows/python-app.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index e733c4e73..f2a508d4c 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -296,10 +296,10 @@ jobs: print(data["project"]["name"]) PY ) - # Wait for PyPI to index the package - sleep 60 - # Install from PyPI - pip install $PACKAGE_NAME + # Wait and retry while PyPI indexes the package + for d in 30 60 120 180 360 720 1080; do + sleep "$d" + pip install "$PACKAGE_NAME" && break || true # Basic import test python -c "import flixopt; print('PyPI installation successful!')" From a29cd603ffd127a3c0687f99a3008d8ff41e0f0d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:32:26 +0200 Subject: [PATCH 14/26] Add retention to report --- .github/workflows/python-app.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index f2a508d4c..c690b7716 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -138,6 +138,7 @@ jobs: name: security-report path: | bandit-report.json + retention-days: 30 create-release: name: Create GitHub Release From 15774f3596193f1da3478236caf0791234410bf4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:35:33 +0200 Subject: [PATCH 15/26] Update folder creation in CalculationResults --- flixopt/calculation.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 32bd3ce7e..2771152db 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -62,12 +62,9 @@ def __init__( self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) self.results: Optional[CalculationResults] = None - try: - self.folder.mkdir(parents=False, exist_ok=True) - except FileNotFoundError as e: - raise FileNotFoundError( - f'Folder {self.folder} and its parent do not exist. Please create them first.' - ) from e + if self.folder.exists() and not self.folder.is_dir(): + raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.') from e + self.folder.mkdir(parents=False, exist_ok=True) @property def main_results(self) -> Dict[str, Union[Scalar, Dict]]: From 71d65d5a6b5c2be1c0dfe86ae01c5b27d8c7cfab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:39:10 +0200 Subject: [PATCH 16/26] ruff format --- .github/workflows/python-app.yaml | 2 +- flixopt/calculation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index c690b7716..fc1a57690 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -352,4 +352,4 @@ jobs: VERSION=${GITHUB_REF#refs/tags/v} echo "Deploying docs after successful PyPI publish: $VERSION" mike deploy --push --update-aliases $VERSION latest - mike set-default --push latest \ No newline at end of file + mike set-default --push latest diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 2771152db..f22f09af6 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -63,7 +63,7 @@ def __init__( self.results: Optional[CalculationResults] = None if self.folder.exists() and not self.folder.is_dir(): - raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.') from e + raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.') self.folder.mkdir(parents=False, exist_ok=True) @property From 760cf19aead6790429c2c3e45337ae4a6e8b31cf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:44:07 +0200 Subject: [PATCH 17/26] Fix retention days --- .github/workflows/python-app.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index fc1a57690..5f0d8d4f8 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -136,9 +136,8 @@ jobs: if: always() with: name: security-report - path: | - bandit-report.json - retention-days: 30 + path: bandit-report.json + retention-days: 30 create-release: name: Create GitHub Release From 8ae7c416105b73c1a60025f9b3889d8961072a5e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:45:44 +0200 Subject: [PATCH 18/26] Bugfix --- .github/workflows/python-app.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 5f0d8d4f8..d186c641a 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -300,6 +300,7 @@ jobs: for d in 30 60 120 180 360 720 1080; do sleep "$d" pip install "$PACKAGE_NAME" && break || true + done # Basic import test python -c "import flixopt; print('PyPI installation successful!')" From cd47e466e2c7298a4370e2972059d388cd3ae714 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:46:55 +0200 Subject: [PATCH 19/26] Simplify caching --- .github/workflows/python-app.yaml | 75 ++++++++----------------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index d186c641a..3bfb50ae9 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -33,15 +33,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - # Cache pip dependencies for linting tools - - name: Cache pip dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-lint-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-lint-pip- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Install Ruff run: | @@ -76,18 +70,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - # Cache pip and pytest for each Python version - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cache/pip - .pytest_cache - key: ${{ runner.os }}-test-py${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-test-py${{ matrix.python-version }}- - ${{ runner.os }}-test- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Install dependencies run: | @@ -109,15 +94,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - # Cache security tools - - name: Cache security tools - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-security-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-security-pip- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Install security tools run: | @@ -196,15 +175,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - # Cache build tools - - name: Cache build tools - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-build-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-build-pip- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Install dependencies run: | @@ -258,15 +231,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - # Cache build tools (can reuse from TestPyPI job if same runner) - - name: Cache build tools - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-build-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-build-pip- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Install dependencies run: | @@ -322,15 +289,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - # Cache documentation dependencies - - name: Cache documentation dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-docs-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-docs-pip- + cache: 'pip' + cache-dependency-path: | + pyproject.toml - name: Sync changelog to docs run: | From 5066b9a4c10e6bb4fd9ada3ceac984b3f0a6d159 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:53:56 +0200 Subject: [PATCH 20/26] Improve version check and publish --- .github/workflows/python-app.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 3bfb50ae9..56fbf6a42 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -263,12 +263,17 @@ 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 - for d in 30 60 120 180 360 720 1080; do + for d in 15 30 60 120 180 360 720 1080; do sleep "$d" - pip install "$PACKAGE_NAME" && break || true + # Install specific version and verify it matches + pip install "$PACKAGE_NAME==$VERSION" && \ + python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed" && \ + break || true done - # Basic import test + # Final success confirmation python -c "import flixopt; print('PyPI installation successful!')" deploy-docs: From 4e6b235d6f3f05e2605cf0cdbbf18f389ef8c237 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:55:10 +0200 Subject: [PATCH 21/26] Also for testpy --- .github/workflows/python-app.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 56fbf6a42..5b2fa1e5a 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -207,12 +207,18 @@ jobs: print(data["project"]["name"]) PY ) - # Install from TestPyPI with retry (TestPyPI can be slow to index) - pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME || \ - (sleep 30 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) || \ - (sleep 60 && pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE_NAME) - # Basic import test - python -c "import flixopt; print('Installation successful!')" + # Extract version from git tag + VERSION=${GITHUB_REF#refs/tags/v} + # Wait and retry while TestPyPI indexes the package + for d in 15 30 60 120 180 360 720 1080; do + sleep "$d" + # Install specific version and verify it matches + pip 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" && \ + break || true + done + # Final success confirmation + python -c "import flixopt; print('TestPyPI installation successful!')" publish-pypi: name: Publish to PyPI From e1887bfbcb6887a5bf81562072452b2e143a44d8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:59:46 +0200 Subject: [PATCH 22/26] Check installation more carefully --- .github/workflows/python-app.yaml | 44 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 5b2fa1e5a..7c9a5c4fe 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -210,13 +210,28 @@ jobs: # 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 sleep "$d" + echo "Attempting to install $PACKAGE_NAME==$VERSION from TestPyPI (retry after ${d}s)..." # Install specific version and verify it matches - pip 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" && \ - break || true + if pip 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 + fi done + + # Check if installation succeeded + if [ "$INSTALL_SUCCESS" = "false" ]; then + echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from TestPyPI after all retries" + echo "This could indicate:" + echo " - TestPyPI indexing issues" + echo " - Package upload problems" + echo " - Version mismatch between tag and package" + exit 1 + fi + # Final success confirmation python -c "import flixopt; print('TestPyPI installation successful!')" @@ -272,13 +287,28 @@ jobs: # Extract version from git tag VERSION=${GITHUB_REF#refs/tags/v} # Wait and retry while PyPI indexes the package - for d in 15 30 60 120 180 360 720 1080; do + INSTALL_SUCCESS=false + for d in 5 10 15 30 60 120 180 360 720 1080; do sleep "$d" + echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." # Install specific version and verify it matches - pip install "$PACKAGE_NAME==$VERSION" && \ - python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed" && \ - break || true + if pip 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 + fi done + + # Check if installation succeeded + if [ "$INSTALL_SUCCESS" = "false" ]; then + echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from PyPI after all retries" + echo "This could indicate:" + echo " - PyPI indexing issues" + echo " - Package upload problems" + echo " - Version mismatch between tag and package" + exit 1 + fi + # Final success confirmation python -c "import flixopt; print('PyPI installation successful!')" From 2e44a0968abe73027c6dd6d9a4ce71f0a6e1a3ce Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:35:40 +0200 Subject: [PATCH 23/26] Pin ruff in workflow --- .github/workflows/python-app.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 7c9a5c4fe..5196d7815 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -40,7 +40,8 @@ jobs: - name: Install Ruff run: | python -m pip install --upgrade pip - pip install ruff + pip install "ruff==0.9.*" + ruff --version - name: Run Ruff Linting run: | From 6c12427a8c870dfa022eb7a0222f5eb3d2d860d5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:35:50 +0200 Subject: [PATCH 24/26] Add timeout to workflow --- .github/workflows/python-app.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 5196d7815..2f1f11e48 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -57,6 +57,7 @@ jobs: test: runs-on: ubuntu-22.04 + timeout-minutes: 30 needs: lint # Run tests only after linting passes strategy: fail-fast: false # Continue testing other Python versions if one fails From 653254445f7dca94ddb63e585a15f88963c8626a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:36:03 +0200 Subject: [PATCH 25/26] Remove duplicate --- .github/workflows/python-app.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 2f1f11e48..74dac04b8 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -139,11 +139,6 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Sync changelog to docs - run: | - cp CHANGELOG.md docs/changelog.md - echo "✅ Synced changelog to docs" - - name: Extract release notes run: | VERSION=${GITHUB_REF#refs/tags/v} From 369ee353026b5ab8869793202bf7c3c5fa1e2ce5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:36:18 +0200 Subject: [PATCH 26/26] Fix testpypi (needs link) --- .github/workflows/python-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 74dac04b8..0ff4fc0ff 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -187,7 +187,7 @@ jobs: - name: Upload to TestPyPI run: | - twine upload --repository testpypi dist/* --verbose + twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}