Production Release #2
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: Production Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to release (e.g., v1.0.0 or 25.5.28)' | |
| required: true | |
| type: string | |
| create_tag: | |
| description: 'Create tag if it does not exist' | |
| required: false | |
| default: false | |
| type: boolean | |
| prerelease: | |
| description: 'Mark as pre-release' | |
| required: false | |
| default: false | |
| type: boolean | |
| jobs: | |
| validate-tag: | |
| name: Validate and prepare tag | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.tag.outputs.tag }} | |
| tag_exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for tags | |
| - name: Validate tag format | |
| id: tag | |
| run: | | |
| TAG="${{ github.event.inputs.tag }}" | |
| if [[ ! "$TAG" =~ ^(v?[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then | |
| echo "Error: Tag must be in format v1.0.0, 1.0.0, or CalVer like 25.5.28" | |
| exit 1 | |
| fi | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| - name: Check if tag exists | |
| id: check | |
| run: | | |
| if git rev-parse "refs/tags/${{ steps.tag.outputs.tag }}" >/dev/null 2>&1; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| update-citation: | |
| name: Update CITATION.cff | |
| needs: validate-tag | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required for pushing commits | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for tags | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update CITATION.cff | |
| run: | | |
| # Get current date in YYYY-MM-DD format | |
| TODAY=$(date +"%Y-%m-%d") | |
| # Extract version without 'v' prefix | |
| VERSION="${{ needs.validate-tag.outputs.tag }}" | |
| VERSION=${VERSION#v} | |
| # Update version and date in CITATION.cff | |
| python -c " | |
| import re | |
| with open('CITATION.cff', 'r') as f: | |
| content = f.read() | |
| # Update version | |
| content = re.sub(r'version: \".*\"', f'version: \"$VERSION\"', content) | |
| # Update date-released | |
| content = re.sub(r'date-released: \'.*\'', f'date-released: \'$TODAY\'', content) | |
| with open('CITATION.cff', 'w') as f: | |
| f.write(content) | |
| " | |
| # Configure git | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Commit CITATION.cff changes if any | |
| if ! git diff --quiet CITATION.cff; then | |
| git add CITATION.cff | |
| git commit -m "Update CITATION.cff to version $VERSION [skip ci]" | |
| git push origin HEAD | |
| else | |
| echo "No changes to CITATION.cff" | |
| fi | |
| create-tag: | |
| name: Create tag if needed | |
| needs: [validate-tag, update-citation] | |
| runs-on: ubuntu-latest | |
| if: needs.validate-tag.outputs.tag_exists == 'false' && github.event.inputs.create_tag == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: main # Get the latest main branch with CITATION.cff updates | |
| - name: Create and push tag | |
| run: | | |
| # Configure git | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Create and push tag | |
| git tag ${{ needs.validate-tag.outputs.tag }} | |
| git push origin ${{ needs.validate-tag.outputs.tag }} | |
| build: | |
| name: Build distribution 📦 | |
| needs: [validate-tag, update-citation, create-tag] | |
| if: always() && needs.validate-tag.result == 'success' && needs.update-citation.result == 'success' && (needs.create-tag.result == 'success' || needs.create-tag.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # If tag exists, use it; otherwise use main branch with updated CITATION.cff | |
| ref: ${{ needs.validate-tag.outputs.tag_exists == 'true' && needs.validate-tag.outputs.tag || 'main' }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.x" | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install build | |
| - name: Build distribution | |
| run: python -m build | |
| - name: Store the distribution packages | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| publish-to-pypi: | |
| name: Publish to PyPI 🚀 | |
| needs: [validate-tag, build] | |
| if: always() && needs.validate-tag.result == 'success' && needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/funannotate2 | |
| permissions: | |
| id-token: write # IMPORTANT: mandatory for trusted publishing | |
| steps: | |
| - name: Download distribution packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Publish to PyPI | |
| id: pypi_publish | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| continue-on-error: true | |
| - name: Check PyPI publish result | |
| run: | | |
| if [ "${{ steps.pypi_publish.outcome }}" == "failure" ]; then | |
| echo "⚠️ PyPI upload failed - this is expected if version already exists" | |
| echo "Checking if version exists on PyPI..." | |
| # Check if package exists on PyPI | |
| if curl -s "https://pypi.org/pypi/funannotate2/${{ needs.validate-tag.outputs.tag }}/json" | grep -q "Not Found"; then | |
| echo "❌ Version does not exist on PyPI - this is a real error" | |
| exit 1 | |
| else | |
| echo "✅ Version already exists on PyPI - continuing with GitHub release" | |
| fi | |
| else | |
| echo "✅ PyPI upload successful" | |
| fi | |
| create-github-release: | |
| name: Create GitHub Release | |
| needs: [validate-tag, build, publish-to-pypi] | |
| if: always() && needs.validate-tag.result == 'success' && needs.build.result == 'success' && (needs.publish-to-pypi.result == 'success' || needs.publish-to-pypi.result == 'failure') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required for creating releases | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.validate-tag.outputs.tag }} | |
| fetch-depth: 0 | |
| - name: Download distribution packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Generate release notes | |
| id: release_notes | |
| run: | | |
| # Get the previous tag | |
| PREV_TAG=$(git describe --tags --abbrev=0 ${{ needs.validate-tag.outputs.tag }}^) | |
| echo "Generating release notes for ${{ needs.validate-tag.outputs.tag }}" | |
| echo "Previous tag: $PREV_TAG" | |
| echo "" | |
| # Generate changelog | |
| echo "## Changes" > release_notes.md | |
| echo "" >> release_notes.md | |
| # Get all commits between previous tag and current tag, excluding version bump commits | |
| git log --pretty=format:"- %s (%h)" $PREV_TAG..${{ needs.validate-tag.outputs.tag }} \ | |
| --grep="bump version" --grep="version bump" --grep="update version" \ | |
| --invert-grep >> release_notes.md | |
| # Check if we have any commits | |
| if [ ! -s release_notes.md ] || [ $(wc -l < release_notes.md) -le 2 ]; then | |
| echo "## Changes" > release_notes.md | |
| echo "" >> release_notes.md | |
| echo "- No significant changes since previous release" >> release_notes.md | |
| fi | |
| echo "" >> release_notes.md | |
| echo "" >> release_notes.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/$PREV_TAG...${{ needs.validate-tag.outputs.tag }}" >> release_notes.md | |
| echo "Generated release notes:" | |
| cat release_notes.md | |
| - name: Create GitHub Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ needs.validate-tag.outputs.tag }} | |
| name: funannotate2 ${{ needs.validate-tag.outputs.tag }} | |
| body_path: release_notes.md | |
| files: dist/* | |
| prerelease: ${{ github.event.inputs.prerelease == 'true' }} | |
| generate_release_notes: false | |
| fail_on_unmatched_files: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| continue-on-error: true | |
| - name: Fallback - Create Release with GitHub CLI | |
| if: steps.create_release.outcome == 'failure' | |
| run: | | |
| echo "Action failed, trying with GitHub CLI..." | |
| # Create release | |
| if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then | |
| PRERELEASE_FLAG="--prerelease" | |
| else | |
| PRERELEASE_FLAG="" | |
| fi | |
| gh release create "${{ needs.validate-tag.outputs.tag }}" \ | |
| --title "funannotate2 ${{ needs.validate-tag.outputs.tag }}" \ | |
| --notes-file release_notes.md \ | |
| $PRERELEASE_FLAG \ | |
| dist/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |