treemapper CD #33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # .github/workflows/cd.yml | |
| name: treemapper CD | |
| permissions: | |
| contents: write | |
| 'on': | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g., 1.0.0)' | |
| required: true | |
| publish_to_pypi: | |
| description: 'Publish to PyPI' | |
| required: true | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| jobs: | |
| prepare-version: | |
| name: Prepare Version Commit | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.set_outputs.outputs.version }} | |
| tag_name: ${{ steps.set_outputs.outputs.tag_name }} | |
| commit_sha: ${{ steps.commit_version.outputs.commit_sha }} | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Need full history for git bundle | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Check that we're on main branch | |
| run: | | |
| CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) | |
| if [ "$CURRENT_BRANCH" != "main" ]; then | |
| echo "Error: Releases can only be created from the main branch. Current branch: $CURRENT_BRANCH" | |
| exit 1 | |
| fi | |
| - name: Validate version format | |
| run: | | |
| VERSION="${{ github.event.inputs.version }}" | |
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then | |
| echo "Error: Invalid version format '$VERSION'. Expected semver like 1.0.0 or 1.0.0-rc1" | |
| exit 1 | |
| fi | |
| echo "Version format valid: $VERSION" | |
| - name: Set version in version.py | |
| env: | |
| VERSION: ${{ github.event.inputs.version }} | |
| run: | | |
| echo "Setting version to $VERSION" | |
| python - <<'PY' | |
| import os, re, pathlib | |
| ver = os.environ["VERSION"] | |
| p = pathlib.Path("src/treemapper/version.py") | |
| s = p.read_text(encoding="utf-8") | |
| s, n = re.subn(r'__version__\s*=\s*["\'].*?["\']', f'__version__ = "{ver}"', s, count=1) | |
| assert n == 1, "version assignment not found or multiple matches" | |
| p.write_text(s, encoding="utf-8") | |
| PY | |
| echo "version.py content after change:" | |
| cat src/treemapper/version.py | |
| - name: Commit version bump (locally only, no push yet) | |
| id: commit_version | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add src/treemapper/version.py | |
| if ! git diff --staged --quiet; then | |
| git commit -m "Release version ${{ github.event.inputs.version }}" | |
| else | |
| echo "No changes to commit." | |
| fi | |
| COMMIT_SHA=$(git rev-parse HEAD) | |
| echo "Commit SHA: $COMMIT_SHA" | |
| echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT | |
| - name: Create local tag (no push yet) | |
| run: | | |
| git tag -a "v${{ github.event.inputs.version }}" -m "Release version ${{ github.event.inputs.version }}" | |
| echo "Tag created locally: v${{ github.event.inputs.version }}" | |
| - name: Upload git bundle for other jobs | |
| run: | | |
| git bundle create repo.bundle --all | |
| - name: Upload bundle as artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: git-repo-bundle | |
| path: repo.bundle | |
| retention-days: 1 | |
| - name: Set outputs | |
| id: set_outputs | |
| run: | | |
| echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT | |
| echo "tag_name=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT | |
| build-assets: | |
| name: Build Assets | |
| needs: prepare-version | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| asset_name: linux | |
| python-version: '3.11' | |
| - os: macos-latest | |
| asset_name: macos | |
| python-version: '3.11' | |
| - os: windows-latest | |
| asset_name: windows | |
| python-version: '3.11' | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Download git bundle | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: git-repo-bundle | |
| - name: Restore repository from bundle | |
| run: | | |
| git clone repo.bundle repo | |
| cd repo | |
| git checkout ${{ needs.prepare-version.outputs.tag_name }} | |
| # Debug: verify we're in the right state | |
| echo "Current directory: $(pwd)" | |
| echo "Current tag: $(git describe --tags --exact-match 2>/dev/null || echo 'no tag')" | |
| echo "Current commit: $(git rev-parse HEAD)" | |
| echo "Version file content:" | |
| cat src/treemapper/version.py || echo "ERROR: version.py not found!" | |
| echo "Directory structure:" | |
| ls -la src/treemapper/ || echo "ERROR: src/treemapper not found!" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Cache pip Dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip-${{ matrix.python-version }}- | |
| ${{ runner.os }}-pip- | |
| - name: Install Dependencies (including PyInstaller) | |
| working-directory: ./repo | |
| run: | | |
| python -m pip install --upgrade pip | |
| # Install in editable mode to ensure version.py changes are picked up | |
| pip install -e .[dev] | |
| # Verify the installed version matches what we expect | |
| echo "Verifying installed version:" | |
| python -c "from treemapper.version import __version__; print(f'Version in module: {__version__}')" | |
| echo "Expected version: ${{ needs.prepare-version.outputs.version }}" | |
| - name: Build with PyInstaller | |
| working-directory: ./repo | |
| run: | | |
| # Ensure PyInstaller can find the modules | |
| export PYTHONPATH="${PWD}/src:${PYTHONPATH}" | |
| echo "PYTHONPATH: $PYTHONPATH" | |
| # Run PyInstaller with explicit paths | |
| python -m PyInstaller --clean -y --dist ./dist/${{ matrix.asset_name }} --paths ./src treemapper.spec | |
| # Verify the built executable exists (cross-platform) | |
| echo "Checking for built executable in: ./dist/${{ matrix.asset_name }}/" | |
| ls -la "./dist/${{ matrix.asset_name }}/" || echo "ERROR: dist directory not found!" | |
| - name: Determine architecture | |
| id: arch | |
| shell: bash | |
| run: | | |
| ARCH=$(uname -m) | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| if [[ "${{ runner.arch }}" == "X64" ]]; then ARCH="x86_64"; \ | |
| elif [[ "${{ runner.arch }}" == "ARM64" ]]; then ARCH="arm64"; \ | |
| else ARCH="unknown"; fi | |
| elif [[ "${{ runner.os }}" == "macOS" ]] && [[ "$ARCH" == "arm64" ]]; then | |
| echo "Detected ARM on macOS" | |
| fi | |
| echo "Determined ARCH: $ARCH" | |
| echo "arch=$ARCH" >> $GITHUB_OUTPUT | |
| - name: Rename asset with proper name | |
| shell: bash | |
| working-directory: ./repo | |
| run: | | |
| ASSET_NAME="treemapper-${{ matrix.asset_name }}-${{ steps.arch.outputs.arch }}" | |
| ASSET_NAME="${ASSET_NAME}-v${{ needs.prepare-version.outputs.version }}" | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| ASSET_NAME="${ASSET_NAME}.exe" | |
| mv "./dist/${{ matrix.asset_name }}/treemapper.exe" "./dist/${ASSET_NAME}" | |
| else | |
| mv "./dist/${{ matrix.asset_name }}/treemapper" "./dist/${ASSET_NAME}" | |
| fi | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: ${{ matrix.asset_name }}-binary | |
| path: ./repo/dist/treemapper-* | |
| retention-days: 1 | |
| publish-to-pypi: | |
| name: Publish to PyPI | |
| needs: [prepare-version, build-assets] | |
| if: github.event.inputs.publish_to_pypi == 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/treemapper | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download git bundle | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: git-repo-bundle | |
| - name: Restore repository from bundle | |
| run: | | |
| git clone repo.bundle repo | |
| cd repo | |
| git checkout ${{ needs.prepare-version.outputs.tag_name }} | |
| # Debug: verify we're in the right state | |
| echo "Current directory: $(pwd)" | |
| echo "Current tag: $(git describe --tags --exact-match 2>/dev/null || echo 'no tag')" | |
| echo "Current commit: $(git rev-parse HEAD)" | |
| echo "Version file content:" | |
| cat src/treemapper/version.py || echo "ERROR: version.py not found!" | |
| echo "Directory structure:" | |
| ls -la src/treemapper/ || echo "ERROR: src/treemapper not found!" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install build tools | |
| working-directory: ./repo | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build | |
| # Verify version.py has the correct version | |
| echo "Version in version.py:" | |
| cat src/treemapper/version.py | |
| echo "Expected version: ${{ needs.prepare-version.outputs.version }}" | |
| - name: Build sdist and wheel | |
| working-directory: ./repo | |
| run: | | |
| # Build the distribution packages | |
| python -m build | |
| # Verify the built packages have correct version in filename | |
| echo "Built packages:" | |
| ls -la dist/ | |
| VERSION="${{ needs.prepare-version.outputs.version }}" | |
| echo "Looking for version ${VERSION} in package names..." | |
| ls dist/*${VERSION}* || echo "WARNING: No packages found!" | |
| - name: Publish package distributions to PyPI | |
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 | |
| with: | |
| packages-dir: ./repo/dist/ | |
| finalize-release: | |
| name: Push Release and Create GitHub Release | |
| needs: [prepare-version, build-assets, publish-to-pypi] | |
| if: | | |
| needs.prepare-version.result == 'success' && | |
| needs.build-assets.result == 'success' && | |
| (needs.publish-to-pypi.result == 'success' || | |
| needs.publish-to-pypi.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download git bundle | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: git-repo-bundle | |
| - name: Restore repository from bundle | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git clone repo.bundle repo | |
| cd repo | |
| git checkout main | |
| echo "Current branch: $(git branch --show-current)" | |
| echo "Current commit: $(git rev-parse HEAD)" | |
| # Set up remote to point to the actual GitHub repository | |
| git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" | |
| # Verify remote is set up correctly | |
| git remote -v | |
| - name: Push commit and tag to main | |
| working-directory: ./repo | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # Push the version bump commit to main | |
| git push origin HEAD:main | |
| # Push the tag | |
| git push origin ${{ needs.prepare-version.outputs.tag_name }} | |
| echo "Successfully pushed version commit and tag" | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| path: ./artifacts | |
| - name: Create GitHub Release with assets | |
| uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2 | |
| with: | |
| tag_name: ${{ needs.prepare-version.outputs.tag_name }} | |
| name: Release ${{ needs.prepare-version.outputs.version }} | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: true | |
| files: ./artifacts/**/*-binary/treemapper-* |