name: CI/CD env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" on: push: branches: [main, develop] tags: - "v*.*.*" paths-ignore: - "*.md" - "LICENSE" - "docs/**" - ".gitignore" pull_request: branches: [main] paths-ignore: - "*.md" - "LICENSE" - "docs/**" - ".gitignore" release: types: [published] jobs: test: permissions: contents: read runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.12", "3.13"] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] - name: Lint with ruff run: | ruff check src tests - name: Format check with ruff run: | ruff format --check src tests - name: Run tests run: | pytest --cov=tzst --cov-branch --cov-report=xml - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: xixu-me/tzst build: needs: test permissions: contents: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build - name: Check package run: twine check dist/* - name: Upload build artifacts uses: actions/upload-artifact@v7 with: name: dist path: dist/ publish: needs: build runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') environment: name: pypi url: https://pypi.org/p/tzst permissions: id-token: write steps: - name: Download build artifacts uses: actions/download-artifact@v8 with: name: dist path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 build-binaries: needs: test if: startsWith(github.ref, 'refs/tags/v') permissions: contents: read strategy: matrix: include: # Linux architectures - os: ubuntu-latest os_name: linux arch: amd64 python-version: "3.12" - os: ubuntu-latest os_name: linux arch: arm64 python-version: "3.12" cross_compile: true # Windows architectures - os: windows-latest os_name: windows arch: amd64 python-version: "3.12" - os: windows-latest os_name: windows arch: arm64 python-version: "3.12" cross_compile: true # macOS architectures - os: macos-15-intel # Intel-based runner os_name: darwin arch: amd64 python-version: "3.12" - os: macos-15 # ARM-based runner (M1/M2) os_name: darwin arch: arm64 python-version: "3.12" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Extract version from tag id: version shell: bash run: | VERSION=${GITHUB_REF#refs/tags/v} echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Version: $VERSION" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e . pip install pyinstaller - name: Build binary run: | python -m PyInstaller --onefile --name tzst --console --icon docs/_static/favicon.ico src/main.py - name: Verify binary (Linux/macOS) if: matrix.os != 'windows-latest' run: | if [ ! -f "dist/tzst" ]; then echo "Binary build failed!" exit 1 fi - name: Verify binary (Windows) if: matrix.os == 'windows-latest' run: | if (-not (Test-Path "dist/tzst.exe")) { Write-Host "Binary build failed!" exit 1 } - name: Build binary for ARM64 on macOS if: matrix.os_name == 'darwin' && matrix.arch == 'arm64' run: | python -m PyInstaller --onefile --name tzst --console --target-arch arm64 --icon docs/_static/favicon.ico src/main.py - name: Build binary for ARM64 on Windows if: matrix.os == 'windows-latest' && matrix.arch == 'arm64' run: | python -m PyInstaller --onefile --name tzst --console --target-arch arm64 --icon docs/_static/favicon.ico src/main.py - name: Setup cross-compilation for Linux ARM64 if: matrix.os == 'ubuntu-latest' && matrix.arch == 'arm64' run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu - name: Build binary for ARM64 on Linux if: matrix.os == 'ubuntu-latest' && matrix.arch == 'arm64' env: CC: aarch64-linux-gnu-gcc run: | python -m PyInstaller --onefile --name tzst --console --target-arch aarch64 --icon docs/_static/favicon.ico src/main.py - name: Prepare archive contents (Linux/macOS) if: matrix.os != 'windows-latest' run: | mkdir -p archive cp dist/tzst archive/ cp README.md archive/ cp LICENSE archive/ - name: Prepare archive contents (Windows) if: matrix.os == 'windows-latest' run: | New-Item -ItemType Directory -Path archive -Force Copy-Item dist/tzst.exe archive/ Copy-Item README.md archive/ Copy-Item LICENSE archive/ - name: Create zip archive (Linux/macOS) if: matrix.os != 'windows-latest' run: | cd archive zip -r ../tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip . - name: Create zip archive (Windows) if: matrix.os == 'windows-latest' run: | cd archive Compress-Archive -Path * -DestinationPath ../tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip - name: Upload binary artifacts uses: actions/upload-artifact@v7 with: name: binary-${{ matrix.os_name }}-${{ matrix.arch }} path: tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip create-release: needs: [publish, build-binaries] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - name: Extract version from tag id: version run: | VERSION=${GITHUB_REF#refs/tags/v} echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Download all binary artifacts uses: actions/download-artifact@v8 with: pattern: binary-* merge-multiple: true - name: Create GitHub Release run: | gh release create ${{ github.ref_name }} \ --title "tzst ${{ steps.version.outputs.version }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Release Assets run: | echo "Available files:" ls -la tzst-${{ steps.version.outputs.version }}-*.zip 2>/dev/null || echo "No zip files found" success=0 for file in tzst-${{ steps.version.outputs.version }}-*.zip; do if [ -f "$file" ]; then echo "Uploading $file" if gh release upload ${{ github.ref_name }} "$file" --clobber; then echo "Successfully uploaded $file" success=$((success + 1)) else echo "Failed to upload $file" fi fi done if [ $success -eq 0 ]; then echo "Warning: No files were successfully uploaded" else echo "Successfully uploaded $success file(s)" fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} update_badges: needs: [build, publish] if: always() && needs.build.result == 'success' && startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 ref: main - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Fetch PyPI version id: pypi_version run: | pip install requests python -c " import requests, json, os try: data = requests.get('https://pypi.org/pypi/tzst/json').json() version = data['info']['version'] print(f'PyPI version: {version}') except Exception as e: print(f'Error fetching version: {e}') version = 'unknown' with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'version={version}\n') " - name: Update README badge run: | echo "Updating PyPI badge with version: ${{ steps.pypi_version.outputs.version }}" # Update PyPI version badge in README.md if [ -f "README.md" ]; then # Replace PyPI version badge sed -i 's|https://img.shields.io/pypi/v/tzst[^)]*|https://img.shields.io/pypi/v/tzst|g' README.md || true # Update version in badge alt text if exists sed -i 's|PyPI - Version[^]]*|PyPI - Version|g' README.md || true fi git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - name: Commit and push changes uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: "Update badges [skip ci]" file_pattern: README.md branch: main