fix: resolve automated release system inconsistencies and improve wor… #11
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
| name: Automated Release Pipeline | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths-ignore: | |
| - 'README.md' | |
| - 'docs/**' | |
| - '.gitignore' | |
| - 'LICENSE' | |
| # Grant necessary permissions for the workflow | |
| permissions: | |
| contents: write # Required to push commits and create tags | |
| actions: write # Required to trigger other workflows | |
| packages: write # Required for package publishing | |
| pull-requests: write # Required for PR operations | |
| issues: write # Required for issue operations | |
| env: | |
| PYTHON_VERSION: '3.11' | |
| jobs: | |
| analyze-and-version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_release: ${{ steps.version_check.outputs.should_release }} | |
| version_type: ${{ steps.version_check.outputs.version_type }} | |
| new_version: ${{ steps.version_check.outputs.new_version }} | |
| current_version: ${{ steps.version_check.outputs.current_version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Ensure the token can push to protected branches | |
| persist-credentials: true | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Configure Git | |
| run: | | |
| git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| # Configure git to use the GitHub token for authentication | |
| git config --local url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Check Rust formatting | |
| run: cargo fmt --all -- --check | |
| - name: Run Rust linting | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Analyze commits and determine version bump | |
| id: version_check | |
| run: | | |
| # Get current version | |
| CURRENT_VERSION=$(python scripts/get_version.py) | |
| echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| echo "Current version: $CURRENT_VERSION" | |
| # Get commits since last tag | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LAST_TAG" ]; then | |
| echo "No previous tags found, analyzing all commits" | |
| COMMITS=$(git log --oneline) | |
| else | |
| echo "Last tag: $LAST_TAG" | |
| COMMITS=$(git log ${LAST_TAG}..HEAD --oneline) | |
| fi | |
| echo "Analyzing commits since $LAST_TAG:" | |
| echo "$COMMITS" | |
| echo "Number of commits to analyze: $(echo "$COMMITS" | wc -l)" | |
| # Determine version bump type based on commit messages | |
| VERSION_TYPE="none" | |
| # Check for breaking changes (major version) | |
| if echo "$COMMITS" | grep -qiE "(BREAKING CHANGE|breaking:|major:)"; then | |
| VERSION_TYPE="major" | |
| echo "Found breaking change commits - will bump major version" | |
| # Check for new features (minor version) | |
| elif echo "$COMMITS" | grep -qiE "(feat:|feature:|minor:)"; then | |
| VERSION_TYPE="minor" | |
| echo "Found feature commits - will bump minor version" | |
| # Check for bug fixes and other changes (patch version) | |
| elif echo "$COMMITS" | grep -qiE "(fix:|patch:|chore:|docs:|style:|refactor:|perf:|test:)"; then | |
| VERSION_TYPE="patch" | |
| echo "Found fix/maintenance commits - will bump patch version" | |
| fi | |
| echo "Determined version bump type: $VERSION_TYPE" | |
| # Skip release if no relevant changes | |
| if [ "$VERSION_TYPE" = "none" ]; then | |
| echo "No version-relevant changes detected. Skipping release." | |
| echo "should_release=false" >> $GITHUB_OUTPUT | |
| echo "version_type=none" >> $GITHUB_OUTPUT | |
| echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "Determined version bump type: $VERSION_TYPE" | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| echo "version_type=$VERSION_TYPE" >> $GITHUB_OUTPUT | |
| # Calculate new version | |
| echo "Running: python scripts/bump_version.py $VERSION_TYPE" | |
| python scripts/bump_version.py $VERSION_TYPE | |
| NEW_VERSION=$(python scripts/get_version.py) | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "New version will be: $NEW_VERSION" | |
| # Show what files were changed | |
| echo "Files changed by version bump:" | |
| git status --porcelain | |
| - name: Commit version changes | |
| if: steps.version_check.outputs.should_release == 'true' | |
| run: | | |
| # Check if there are changes to commit (check working directory, not staged) | |
| if git diff --quiet; then | |
| echo "No changes to commit" | |
| else | |
| echo "Committing version changes..." | |
| git add -A | |
| git commit -m "chore: bump version to ${{ steps.version_check.outputs.new_version }} [skip ci]" | |
| # Push with retry logic | |
| for i in {1..3}; do | |
| if git push origin main; then | |
| echo "[OK] Successfully pushed version bump commit" | |
| break | |
| else | |
| echo "[ERROR] Push attempt $i failed, retrying in 5 seconds..." | |
| sleep 5 | |
| fi | |
| if [ $i -eq 3 ]; then | |
| echo "[ERROR] Failed to push after 3 attempts" | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| - name: Create and push tag | |
| if: steps.version_check.outputs.should_release == 'true' | |
| run: | | |
| TAG_NAME="v${{ steps.version_check.outputs.new_version }}" | |
| echo "Creating tag: $TAG_NAME" | |
| # Check if tag already exists | |
| if git tag -l | grep -q "^$TAG_NAME$"; then | |
| echo "[WARN] Tag $TAG_NAME already exists, skipping tag creation" | |
| else | |
| git tag "$TAG_NAME" | |
| echo "[OK] Created tag: $TAG_NAME" | |
| # Push tag with retry logic | |
| for i in {1..3}; do | |
| if git push origin "$TAG_NAME"; then | |
| echo "[OK] Successfully pushed tag: $TAG_NAME" | |
| break | |
| else | |
| echo "[ERROR] Tag push attempt $i failed, retrying in 5 seconds..." | |
| sleep 5 | |
| fi | |
| if [ $i -eq 3 ]; then | |
| echo "[ERROR] Failed to push tag after 3 attempts" | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| build-and-release: | |
| needs: analyze-and-version | |
| if: needs.analyze-and-version.outputs.should_release == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.analyze-and-version.outputs.new_version }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Check Rust formatting | |
| run: cargo fmt --all -- --check | |
| - name: Run Rust linting | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Install maturin | |
| run: pip install maturin | |
| - name: Build wheels | |
| run: maturin build --release --out dist --find-interpreter | |
| - name: Upload wheels | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheels-${{ matrix.os }} | |
| path: dist/*.whl | |
| build-sdist: | |
| needs: analyze-and-version | |
| if: needs.analyze-and-version.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.analyze-and-version.outputs.new_version }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install maturin | |
| run: pip install maturin | |
| - name: Build source distribution | |
| run: maturin sdist --out dist | |
| - name: Upload sdist | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sdist | |
| path: dist/*.tar.gz | |
| publish: | |
| needs: [analyze-and-version, build-and-release, build-sdist] | |
| if: needs.analyze-and-version.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| environment: release | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist-artifacts | |
| - name: Flatten artifacts | |
| run: | | |
| mkdir -p dist | |
| find dist-artifacts -name "*.whl" -exec cp {} dist/ \; | |
| find dist-artifacts -name "*.tar.gz" -exec cp {} dist/ \; | |
| ls -la dist/ | |
| - name: Check if version already exists on PyPI | |
| id: check_version | |
| run: | | |
| VERSION=${{ needs.analyze-and-version.outputs.new_version }} | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| if pip index versions demopy_gb_jj 2>/dev/null | grep -q "$VERSION"; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "[WARN] Version $VERSION already exists on PyPI" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "[OK] Version $VERSION is new, proceeding with upload" | |
| fi | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist/ | |
| password: ${{ secrets.PYPI_API_TOKEN || '' }} | |
| skip-existing: true | |
| verbose: true | |
| create-release: | |
| needs: [analyze-and-version, publish] | |
| if: needs.analyze-and-version.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: v${{ needs.analyze-and-version.outputs.new_version }} | |
| - name: Generate changelog | |
| id: changelog | |
| run: | | |
| # Get the previous tag | |
| CURRENT_TAG="v${{ needs.analyze-and-version.outputs.new_version }}" | |
| PREVIOUS_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "") | |
| echo "Generating changelog from $PREVIOUS_TAG to $CURRENT_TAG" | |
| # Generate changelog content | |
| CHANGELOG="## What's Changed\n\n" | |
| if [ -z "$PREVIOUS_TAG" ]; then | |
| COMMITS=$(git log --oneline --pretty=format:"* %s (%h)" $CURRENT_TAG) | |
| else | |
| COMMITS=$(git log --oneline --pretty=format:"* %s (%h)" ${PREVIOUS_TAG}..${CURRENT_TAG}) | |
| fi | |
| # Categorize commits | |
| FEATURES=$(echo "$COMMITS" | grep -iE "(feat:|feature:)" || true) | |
| FIXES=$(echo "$COMMITS" | grep -iE "(fix:|patch:)" || true) | |
| CHORES=$(echo "$COMMITS" | grep -iE "(chore:|docs:|style:|refactor:|perf:|test:)" || true) | |
| BREAKING=$(echo "$COMMITS" | grep -iE "(BREAKING CHANGE|breaking:|major:)" || true) | |
| if [ ! -z "$BREAKING" ]; then | |
| CHANGELOG="${CHANGELOG}### Breaking Changes\n${BREAKING}\n\n" | |
| fi | |
| if [ ! -z "$FEATURES" ]; then | |
| CHANGELOG="${CHANGELOG}### New Features\n${FEATURES}\n\n" | |
| fi | |
| if [ ! -z "$FIXES" ]; then | |
| CHANGELOG="${CHANGELOG}### Bug Fixes\n${FIXES}\n\n" | |
| fi | |
| if [ ! -z "$CHORES" ]; then | |
| CHANGELOG="${CHANGELOG}### Maintenance\n${CHORES}\n\n" | |
| fi | |
| # Add installation instructions | |
| CHANGELOG="${CHANGELOG}### Installation\n\n" | |
| CHANGELOG="${CHANGELOG}\`\`\`bash\n" | |
| CHANGELOG="${CHANGELOG}pip install demopy_gb_jj==${{ needs.analyze-and-version.outputs.new_version }}\n" | |
| CHANGELOG="${CHANGELOG}\`\`\`\n\n" | |
| # Add usage example | |
| CHANGELOG="${CHANGELOG}### Usage\n\n" | |
| CHANGELOG="${CHANGELOG}\`\`\`python\n" | |
| CHANGELOG="${CHANGELOG}import demopy\n" | |
| CHANGELOG="${CHANGELOG}print(demopy.hello()) # Hello from demopy_gb_jj!\n" | |
| CHANGELOG="${CHANGELOG}print(demopy.add(5, 7)) # 12\n" | |
| CHANGELOG="${CHANGELOG}\`\`\`\n" | |
| # Save changelog to file and output | |
| echo -e "$CHANGELOG" > changelog.md | |
| echo "changelog<<EOF" >> $GITHUB_OUTPUT | |
| echo -e "$CHANGELOG" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ needs.analyze-and-version.outputs.new_version }} | |
| name: Release v${{ needs.analyze-and-version.outputs.new_version }} | |
| body: ${{ steps.changelog.outputs.changelog }} | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: true |