Skip to content

treemapper CD

treemapper CD #33

Workflow file for this run

# .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-*