diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..abd2e5876 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: pip + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 000000000..5c40c2cf8 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,23 @@ +name: Dependabot auto-merge + +on: + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + name: Auto-merge minor/patch + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-24.04 + steps: + - uses: dependabot/fetch-metadata@v2 + id: metadata + + - if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: gh pr merge "$PR" --auto --squash + env: + PR: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml new file mode 100644 index 000000000..3a358dc31 --- /dev/null +++ b/.github/workflows/pr-title.yaml @@ -0,0 +1,32 @@ +name: PR Title + +on: + push: + branches: ["release-please--**"] + pull_request: + types: [opened, edited, reopened] + +jobs: + validate: + name: Validate conventional commit + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + permissions: + pull-requests: read + steps: + - uses: amannn/action-semantic-pull-request@v6 + with: + types: | + feat + fix + refactor + test + docs + chore + ci + build + perf + revert + style + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..fc44659ab --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,121 @@ +name: Publish + +on: + push: + tags: ["v*"] + workflow_call: + inputs: + tag: + required: true + type: string + +env: + PYTHON_VERSION: "3.11" + +jobs: + test: + name: Run tests + uses: ./.github/workflows/tests.yaml + + build: + name: Build package + needs: test + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag || github.ref_name }} + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build package + run: uv build + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + publish-pypi: + name: Publish to PyPI + needs: build + runs-on: ubuntu-24.04 + environment: + name: pypi + url: https://pypi.org/p/flixopt + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + verify-pypi: + name: Verify PyPI installation + needs: publish-pypi + runs-on: ubuntu-24.04 + steps: + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Verify installation + run: | + VERSION="${TAG#v}" + + for delay in 30 60 90 120 180 300; do + sleep $delay + echo "Attempting installation (waited ${delay}s)..." + + if uv pip install --system --index-url https://pypi.org/simple/ "flixopt==$VERSION" && \ + python -c "from importlib.metadata import version; assert version('flixopt') == '$VERSION'"; then + echo "PyPI installation successful!" + exit 0 + fi + done + + echo "Failed to verify PyPI installation" + exit 1 + env: + TAG: ${{ inputs.tag || github.ref_name }} + + github-release: + name: Create GitHub release + # Only on tag push — release-please creates its own release via workflow_call + if: github.event_name == 'push' + needs: verify-pypi + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + + - name: Create GitHub release + run: | + if [[ "$TAG" =~ (rc|alpha|beta) ]]; then + gh release create "$TAG" --generate-notes --prerelease + else + gh release create "$TAG" --generate-notes + fi + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dedad171a..c73df4988 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,40 +2,48 @@ name: Release on: push: - tags: - - v*.*.* + branches: [main] -env: - PYTHON_VERSION: "3.11" - PREPARATION_COMMIT: '[ci] prepare release ${{ github.ref_name }}' +permissions: + contents: write + pull-requests: write jobs: - check-preparation: - name: Check if release is prepared + release-please: + name: Release Please runs-on: ubuntu-24.04 outputs: - prepared: ${{ steps.validate.outputs.prepared }} + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} steps: - - uses: actions/checkout@v6 - - - name: Validate commit message - id: validate - run: | - COMMIT_MESSAGE=$(git log -1 --pretty=%B) - echo "Expected: '${{ env.PREPARATION_COMMIT }}'" - echo "Received: '$COMMIT_MESSAGE'" + - name: Generate token for Release Bot + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} - prepared="false" - if [[ "$COMMIT_MESSAGE" == "${{ env.PREPARATION_COMMIT }}" ]]; then - prepared="true" - fi + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ steps.generate-token.outputs.token }} + config-file: .release-please-config.json + manifest-file: .release-please-manifest.json - echo "prepared=$prepared" >> $GITHUB_OUTPUT + publish: + name: Publish + needs: release-please + if: needs.release-please.outputs.release_created + permissions: + id-token: write + contents: write + uses: ./.github/workflows/publish.yaml + with: + tag: ${{ needs.release-please.outputs.tag_name }} - prepare-release: - name: Prepare release - needs: [check-preparation] - if: needs.check-preparation.outputs.prepared == 'false' + update-citation-date: + name: Update CITATION.cff date + needs: publish runs-on: ubuntu-24.04 steps: - name: Generate token for Release Bot @@ -47,147 +55,25 @@ jobs: - uses: actions/checkout@v6 with: - fetch-depth: 0 ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Configure Git + - name: Update date-released run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Update CITATION.cff - run: | - VERSION=${GITHUB_REF#refs/tags/v} DATE=$(date +%Y-%m-%d) - sed -i "s/^version: .*/version: $VERSION/" CITATION.cff sed -i "s/^date-released: .*/date-released: $DATE/" CITATION.cff - - name: Remove previous tag - run: | - git tag -d ${{ github.ref_name }} - git push origin --delete ${{ github.ref_name }} - - - name: Commit and re-tag + - name: Commit and push run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add CITATION.cff - git commit -m "${{ env.PREPARATION_COMMIT }}" - git push origin main - git tag -a ${{ github.ref_name }} -m "${{ github.ref_name }}" - git push origin ${{ github.ref_name }} - - test: - name: Run tests - needs: [check-preparation] - if: needs.check-preparation.outputs.prepared == 'true' - uses: ./.github/workflows/tests.yaml - - build: - name: Build package - needs: [check-preparation, test] - if: needs.check-preparation.outputs.prepared == 'true' - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - - uses: astral-sh/setup-uv@v7 - with: - version: "0.10.9" - enable-cache: true - - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Build package - run: uv build - - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - retention-days: 7 - - publish-pypi: - name: Publish to PyPI - needs: [build] - runs-on: ubuntu-24.04 - environment: - name: pypi - url: https://pypi.org/p/flixopt - permissions: - id-token: write - steps: - - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - skip-existing: true - - verify-pypi: - name: Verify PyPI installation - needs: [publish-pypi] - runs-on: ubuntu-24.04 - steps: - - uses: astral-sh/setup-uv@v7 - with: - version: "0.10.9" - - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Verify installation - run: | - VERSION=${GITHUB_REF#refs/tags/v} - - for delay in 10 20 40 60 90 120 180 300; do - sleep $delay - echo "Attempting installation (waited ${delay}s)..." - - if uv pip install --system --index-url https://pypi.org/simple/ "flixopt==$VERSION" && \ - python -c "from importlib.metadata import version; assert version('flixopt') == '$VERSION'"; then - echo "PyPI installation successful!" - exit 0 - fi - done - - echo "Failed to verify PyPI installation" - exit 1 - - create-release: - name: Create GitHub release - needs: [verify-pypi] - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Extract release notes - run: | - VERSION=${GITHUB_REF#refs/tags/v} - python scripts/extract_release_notes.py $VERSION > current_release_notes.md - - - 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') }} - generate_release_notes: true + git diff --staged --quiet || (git commit -m "chore: update CITATION.cff date-released" && git push origin main) deploy-docs: name: Deploy documentation - needs: [create-release] + needs: [release-please, publish] uses: ./.github/workflows/docs.yaml with: deploy: true - version: ${{ github.ref_name }} + version: ${{ needs.release-please.outputs.tag_name }} diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 000000000..ed357ff3a --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-component-in-tag": false, + "packages": { + ".": { + "release-type": "simple", + "package-name": "flixopt", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "CITATION.cff" + ] + } + } +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..8ace91523 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "6.1.0" +} diff --git a/CITATION.cff b/CITATION.cff index 889cffa04..44bfdd7ef 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,7 +2,7 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below and consider citing the related publication." type: software title: "flixopt" -version: 6.2.0rc0 +version: 6.2.0rc0 # x-release-please-version date-released: 2026-03-23 url: "https://github.com/flixOpt/flixopt" repository-code: "https://github.com/flixOpt/flixopt" diff --git a/MANIFEST.in b/MANIFEST.in index f6611cdcb..6b55b8523 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,6 +20,5 @@ prune .venv prune venv exclude .gitignore exclude .pre-commit-config.yaml -exclude renovate.json exclude mkdocs.yml exclude test_package.sh diff --git a/pyproject.toml b/pyproject.toml index afec83e45..fec7eee48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,7 @@ exclude = ["tests*", "docs*"] include-package-data = true [tool.setuptools.exclude-package-data] -"*" = ["*.md", ".git*", "*.ipynb", "renovate.json"] +"*" = ["*.md", ".git*", "*.ipynb"] [tool.setuptools_scm] version_scheme = "post-release" diff --git a/renovate.json b/renovate.json deleted file mode 100644 index ded1fbf17..000000000 --- a/renovate.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "extends": [ - ":dependencyDashboard", - ":semanticPrefixFixDepsChoreOthers", - ":ignoreModulesAndTests", - ":semanticCommits", - "group:monorepos", - "group:recommended", - "mergeConfidence:age-confidence-badges", - "replacements:all", - "workarounds:all", - "schedule:earlyMondays" - ], - "automerge": false, - "labels": ["dependencies"], - "rangeStrategy": "widen", - "minimumReleaseAge": "7 days", - "packageRules": [ - { - "description": "Group and automerge dev and docs dependencies", - "matchDepTypes": ["dev", "docs"], - "groupName": "dev dependencies", - "rangeStrategy": "pin", - "minimumReleaseAge": "14 days", - "automerge": true, - "automergeType": "pr", - "separateMinorPatch": false - }, - { - "matchUpdateTypes": ["patch"], - "matchCurrentVersion": "!/^0/", - "automerge": true, - "automergeType": "pr" - }, - { - "description": "CalVer packages (xarray, dask) can have breaking changes in any release - never automerge, longer release age", - "matchPackageNames": ["xarray", "dask"], - "minimumReleaseAge": "14 days", - "schedule": ["* * * * *"], - "labels": ["calver", "breaking-change-risk", "dependencies"], - "prPriority": 10 - }, - { - "description": "Automerge ruff patches despite 0.x version", - "matchPackageNames": ["ruff"], - "matchUpdateTypes": ["patch"], - "automerge": true, - "automergeType": "pr" - } - ] -} diff --git a/scripts/extract_release_notes.py b/scripts/extract_release_notes.py deleted file mode 100644 index 9c31e18d1..000000000 --- a/scripts/extract_release_notes.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -""" -Extract release notes from CHANGELOG.md for a specific version. -Usage: python extract_release_notes.py -""" - -import re -import sys -from pathlib import Path - - -def extract_release_notes(version: str) -> str: - """Extract release notes for a specific version from CHANGELOG.md. - - For pre-release versions (rc, alpha, beta), falls back to base version notes. - E.g., 6.0.0rc5 will use notes from 6.0.0 if no specific rc5 entry exists. - """ - changelog_path = Path('CHANGELOG.md') - - if not changelog_path.exists(): - print('❌ Error: CHANGELOG.md not found', file=sys.stderr) - sys.exit(1) - - content = changelog_path.read_text(encoding='utf-8') - - # Try exact version first, then fall back to base version for pre-releases - versions_to_try = [version] - base_version = re.sub(r'(rc|alpha|beta)\d*$', '', version) - if base_version != version: - versions_to_try.append(base_version) - - for v in versions_to_try: - # Pattern to match version section: ## [2.1.2] - 2025-06-14 or ## [6.0.0] - Upcoming - pattern = rf'## \[{re.escape(v)}\] - [^\n]+\n(.*?)(?=\n## \[|\n\[Unreleased\]|\Z)' - match = re.search(pattern, content, re.DOTALL) - if match: - return match.group(1).strip() - - print(f"❌ Error: No release notes found for version '{version}'", file=sys.stderr) - sys.exit(1) - - -def main(): - if len(sys.argv) != 2: - print('Usage: python extract_release_notes.py ') - print('Example: python extract_release_notes.py 2.1.2') - sys.exit(1) - - version = sys.argv[1] - release_notes = extract_release_notes(version) - print(release_notes) - - -if __name__ == '__main__': - main()