feat: add semantic-release and binary releases (#3)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b78ac58 --- /dev/null +++ b/.github/workflows/release.yml
@@ -0,0 +1,94 @@ +name: Release + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + build-binaries: + needs: test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build binary + run: python scripts/build_binaries.py + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.os }} + path: dist/casbin-cli-* + + release: + needs: [test, build-binaries] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install wheel twine + npm install -g semantic-release @semantic-release/changelog @semantic-release/exec @semantic-release/github + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Move binaries to dist + run: | + mkdir -p dist + find artifacts -name "casbin-cli-*" -exec cp {} dist/ \; + + - name: Semantic Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + #PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: npx semantic-release \ No newline at end of file
diff --git a/.gitignore b/.gitignore index b7faf40..2efd350 100644 --- a/.gitignore +++ b/.gitignore
@@ -205,3 +205,5 @@ marimo/_static/ marimo/_lsp/ __marimo__/ + +.idea
diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..5337483 --- /dev/null +++ b/.releaserc.json
@@ -0,0 +1,38 @@ +{ + "branches": ["master"], + "repositoryUrl": "https://github.com/casbin/casbin-python-cli", + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "python scripts/update_version.py ${nextRelease.version}" + } + ], + [ + "@semantic-release/github", + { + "assets": [ + { + "path": "dist/*.whl", + "label": "Python Wheel" + }, + { + "path": "dist/*.tar.gz", + "label": "Source Distribution" + }, + { + "path": "dist/casbin-cli-*" + } + ] + } + ] + ] +} \ No newline at end of file
diff --git a/README.md b/README.md index f309ec2..8384160 100644 --- a/README.md +++ b/README.md
@@ -33,20 +33,39 @@ ## Project Structure ``` -casbin-python-cli/ -├── casbin_cli/ -│ ├── __init__.py -│ ├── client.py # Main entry point and CLI argument parsing -│ ├── command_executor.py # Dynamic method execution and JSON response -│ ├── enforcer_factory.py # Enforcer creation and input validation -│ ├── response.py # JSON response formatting -│ └── utils.py # Utility functions -├── examples/ # Example configuration files -├── requirements.txt # Python dependencies -├── setup.py # Package installation script -└── README.md +casbin-python-cli/ +├── .github/ +│ └── workflows/ +│ └── release.yml # GitHub Actions CI/CD +├── scripts/ +│ ├── update_version.py # Version management +│ └── build_binaries.py # Binary building +├── casbin_cli/ +│ ├── __init__.py +│ ├── __version__.py # Version source +│ ├── client.py # Main CLI entry point +│ ├── command_executor.py # Command execution +│ ├── enforcer_factory.py # Enforcer creation +│ ├── response.py # Response formatting +│ └── utils.py # Utilities +├── examples/ # Example configurations +├── .releaserc.json # Semantic release config +├── package.json # Node.js dependencies +├── requirements.txt # Python dependencies +├── setup.py # Package setup +└── README.md ``` +### Release Process + +Releases are automated via GitHub Actions: + +1. Push commits to `main` branch +2. Semantic release analyzes commit messages +3. Automatically generates version numbers and changelog +4. Builds cross-platform binaries +5. Publishes to PyPI and GitHub Releases + ## Requirements - Python 3.6+
diff --git a/casbin_cli/__version__.py b/casbin_cli/__version__.py new file mode 100644 index 0000000..d538f87 --- /dev/null +++ b/casbin_cli/__version__.py
@@ -0,0 +1 @@ +__version__ = "1.0.0" \ No newline at end of file
diff --git a/casbin_cli/client.py b/casbin_cli/client.py index 8758843..09e6146 100644 --- a/casbin_cli/client.py +++ b/casbin_cli/client.py
@@ -1,9 +1,10 @@ import argparse import sys import json -from .command_executor import CommandExecutor -from .enforcer_factory import EnforcerFactory -from .utils import process_line_breaks +from casbin_cli.command_executor import CommandExecutor +from casbin_cli.enforcer_factory import EnforcerFactory +from casbin_cli.utils import process_line_breaks +from casbin_cli.__version__ import __version__ class Client: @staticmethod @@ -23,10 +24,10 @@ if command_name in ['-h', '--help']: Client._print_help() return "" - elif command_name in ['-v', '--version']: - print("casbin-python-cli 1.0.0") - print("pycasbin 1.17.0") - return "" + #elif command_name in ['-v', '--version']: + # print(f"casbin-python-cli {__version__}") + # print("pycasbin 1.17.0") + # return "" # Handle line breaks processed_args = [args[0]]
diff --git a/package.json b/package.json new file mode 100644 index 0000000..25736da --- /dev/null +++ b/package.json
@@ -0,0 +1,11 @@ +{ + "name": "casbin-python-cli", + "version": "1.0.0", + "private": true, + "devDependencies": { + "@semantic-release/changelog": "^6.0.0", + "@semantic-release/exec": "^6.0.0", + "@semantic-release/github": "^8.0.0", + "semantic-release": "^19.0.0" + } +} \ No newline at end of file
diff --git a/scripts/build_binaries.py b/scripts/build_binaries.py new file mode 100644 index 0000000..7fa072d --- /dev/null +++ b/scripts/build_binaries.py
@@ -0,0 +1,57 @@ +# Copyright 2025 The casbin Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import subprocess +import platform + +def build_binary(): + """Build standalone binary using PyInstaller""" + + # Install PyInstaller if not present + subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], check=True) + + # Get platform info + system = platform.system().lower() + arch = platform.machine().lower() + + # Build binary + binary_name = f"casbin-cli-{system}-{arch}" + if system == "windows": + binary_name += ".exe" + + cmd = [ + "pyinstaller", + "--onefile", + "--name", binary_name, + "--console", + "--paths", ".", + "--hidden-import", "casbin_cli", + "--hidden-import", "casbin", + "--hidden-import", "casbin_cli.client", + "--hidden-import", "casbin_cli.command_executor", + "--hidden-import", "casbin_cli.enforcer_factory", + "--hidden-import", "casbin_cli.response", + "--hidden-import", "casbin_cli.utils", + "--collect-all", "casbin", + "casbin_cli/client.py" +] + + subprocess.run(cmd, check=True) + + print(f"Binary built successfully: dist/{binary_name}") + +if __name__ == "__main__": + build_binary() \ No newline at end of file
diff --git a/scripts/update_version.py b/scripts/update_version.py new file mode 100644 index 0000000..14bff6e --- /dev/null +++ b/scripts/update_version.py
@@ -0,0 +1,45 @@ +# Copyright 2025 The casbin Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import re +import os + +def update_version(new_version): + """Update version in all relevant files""" + + # Update setup.py + setup_py_path = "setup.py" + with open(setup_py_path, 'r') as f: + content = f.read() + + # Update client.py version display + client_py_path = "casbin_cli/client.py" + with open(client_py_path, 'r') as f: + content = f.read() + + # Create __version__.py + version_py_path = "casbin_cli/__version__.py" + with open(version_py_path, 'w') as f: + f.write(f'__version__ = "{new_version}"\n') + + print(f"Updated version to {new_version}") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python update_version.py <version>") + sys.exit(1) + + new_version = sys.argv[1] + update_version(new_version) \ No newline at end of file
diff --git a/setup.py b/setup.py index 177460b..7bef60a 100644 --- a/setup.py +++ b/setup.py
@@ -1,9 +1,34 @@ +# Copyright 2025 The casbin Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from setuptools import setup, find_packages +import os + +# Read version from __version__.py +version_file = os.path.join(os.path.dirname(__file__), 'casbin_cli', '__version__.py') +if os.path.exists(version_file): + exec(open(version_file).read()) + version = __version__ +else: + version = "1.0.0" setup( name="casbin-python-cli", - version="1.0.0", + version=version, description="A command-line tool for PyCasbin", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", packages=find_packages(), install_requires=[ "casbin>=1.17.0",