name: π Auto Release
on:
push:
branches: [ main, release ]
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
channel:
description: 'Release channel'
required: true
default: 'stable'
type: choice
options:
- stable
- beta
- alpha
permissions:
contents: write
packages: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
NODE_VERSION: '20'
PYTHON_VERSION: '3.11'
jobs:
# Detect if this is a release commit
detect-release:
name: π Detect Release
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
release_type: ${{ steps.check.outputs.release_type }}
channel: ${{ steps.check.outputs.channel }}
new_version: ${{ steps.check.outputs.new_version }}
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: π Check for release commit
id: check
run: |
# Manual workflow dispatch
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "should_release=true" >> $GITHUB_OUTPUT
echo "release_type=${{ github.event.inputs.release_type }}" >> $GITHUB_OUTPUT
echo "channel=${{ github.event.inputs.channel }}" >> $GITHUB_OUTPUT
echo "Manual release triggered: ${{ github.event.inputs.release_type }} (${{ github.event.inputs.channel }})"
exit 0
fi
# Get the commit message
COMMIT_MSG=$(git log -1 --pretty=%B)
echo "Commit message: $COMMIT_MSG"
# Skip release for dependency updates and version bumps
if [[ "$COMMIT_MSG" =~ ^chore\(deps ]] || \
[[ "$COMMIT_MSG" =~ ^Merge\ pull\ request.*dependabot ]] || \
[[ "$COMMIT_MSG" =~ \[skip\ ci\] ]] || \
[[ "$COMMIT_MSG" =~ ^chore:\ bump\ version ]]; then
echo "should_release=false" >> $GITHUB_OUTPUT
echo "βοΈ Skipping release for dependency/maintenance commit"
exit 0
fi
# Use sync-versions.js script for semantic release detection
chmod +x scripts/sync-versions.js
# Check if commit should trigger a release using our semantic detection
RELEASE_OUTPUT=$(node scripts/sync-versions.js detect 2>&1)
if echo "$RELEASE_OUTPUT" | grep -q "should_release=true"; then
echo "$RELEASE_OUTPUT" >> $GITHUB_OUTPUT
echo "π― Semantic release detected"
else
echo "should_release=false" >> $GITHUB_OUTPUT
echo "βΉοΈ No semantic release pattern detected"
fi
# Version bump and synchronization
bump-version:
name: π Bump Version
runs-on: ubuntu-latest
needs: detect-release
if: needs.detect-release.outputs.should_release == 'true'
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: π§ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: π Bump version and sync
id: bump
run: |
echo "π Bumping version: ${{ needs.detect-release.outputs.release_type }} (${{ needs.detect-release.outputs.channel }})"
# Make sync script executable
chmod +x scripts/sync-versions.js
# Bump version
NEW_VERSION=$(node scripts/sync-versions.js release ${{ needs.detect-release.outputs.release_type }} ${{ needs.detect-release.outputs.channel }})
if [ -z "$NEW_VERSION" ]; then
echo "β Failed to bump version"
exit 1
fi
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "β
Version bumped to: $NEW_VERSION"
- name: πΎ Commit version bump
id: commit
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add packages/js/package.json
git add packages/py/pyproject.toml
git add packages/py/glin_profanity/__init__.py
git add package.json
# Check if there are changes to commit
if ! git diff --staged --quiet; then
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} [skip ci]"
# Try to push, but don't fail the entire workflow if it's blocked by repository rules
if git push; then
echo "push_success=true" >> $GITHUB_OUTPUT
echo "β
Version bump committed and pushed successfully"
else
echo "push_success=false" >> $GITHUB_OUTPUT
echo "β οΈ Version bump committed but push was blocked by repository rules"
echo "The release will continue with the locally bumped version files"
fi
else
echo "push_success=true" >> $GITHUB_OUTPUT
echo "βΉοΈ No changes to commit"
fi
# Build packages
build:
name: ποΈ Build Packages
runs-on: ubuntu-latest
needs: [detect-release, bump-version]
if: needs.detect-release.outputs.should_release == 'true'
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: π§ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: π Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: π Re-apply version bump
run: |
# Re-apply the version bump since we're starting from a fresh checkout
echo "π Re-applying version bump to: ${{ needs.bump-version.outputs.new_version }}"
chmod +x scripts/sync-versions.js
node scripts/sync-versions.js release ${{ needs.detect-release.outputs.release_type }} ${{ needs.detect-release.outputs.channel }} > /dev/null
- name: π§ Install root dependencies
run: |
# Install root dependencies to get husky and other tools
npm install --legacy-peer-deps
- name: ποΈ Build JavaScript package
working-directory: packages/js
run: |
npm install --legacy-peer-deps --ignore-scripts
npm run build
npm pack
- name: ποΈ Build Python package
working-directory: packages/py
run: |
pip install hatch
hatch build
- name: πΎ Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: packages-${{ github.sha }}
path: |
packages/js/*.tgz
packages/py/dist/*
retention-days: 7
# Publish to npm
publish-npm:
name: π¦ Publish to npm
runs-on: ubuntu-latest
environment: npm-publish
needs: [detect-release, bump-version, build]
if: always() && needs.detect-release.outputs.should_release == 'true' && needs.build.result == 'success'
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: π§ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: π Re-apply version bump
run: |
# Re-apply the version bump since we're starting from a fresh checkout
echo "π Re-applying version bump to: ${{ needs.bump-version.outputs.new_version }}"
chmod +x scripts/sync-versions.js
node scripts/sync-versions.js release ${{ needs.detect-release.outputs.release_type }} ${{ needs.detect-release.outputs.channel }} > /dev/null
- name: π₯ Download build artifacts
uses: actions/download-artifact@v4
with:
name: packages-${{ github.sha }}
path: ./artifacts/
- name: π§ Upgrade npm for OIDC support
run: npm install -g npm@latest
- name: π§ Install root dependencies
run: |
# Install root dependencies to get husky and other tools
npm install --legacy-peer-deps
- name: π¦ Publish to npm
working-directory: packages/js
run: |
npm install --legacy-peer-deps --ignore-scripts
npm run build
# Get the version from package.json
VERSION=$(node -p "require('./package.json').version")
echo "Current version: $VERSION"
# Check if version already exists on npm
if npm view glin-profanity@$VERSION version 2>/dev/null; then
echo "β οΈ Version $VERSION already exists on npm, skipping publish"
echo "This is expected if the version was already published in a previous run"
exit 0
fi
# Determine npm tag
if [ "${{ needs.detect-release.outputs.channel }}" = "beta" ]; then
NPM_TAG="beta"
elif [ "${{ needs.detect-release.outputs.channel }}" = "alpha" ]; then
NPM_TAG="alpha"
else
NPM_TAG="latest"
fi
echo "Publishing to npm with tag: $NPM_TAG"
# Using OIDC trusted publisher - no token needed
# --provenance adds verified build attestation
npm publish --tag $NPM_TAG --provenance --access public
# Publish to PyPI
publish-pypi:
name: π Publish to PyPI
runs-on: ubuntu-latest
needs: [detect-release, bump-version, build]
if: always() && needs.detect-release.outputs.should_release == 'true' && needs.build.result == 'success'
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: π§ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: π Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: π Re-apply version bump
run: |
# Re-apply the version bump since we're starting from a fresh checkout
echo "π Re-applying version bump to: ${{ needs.bump-version.outputs.new_version }}"
chmod +x scripts/sync-versions.js
node scripts/sync-versions.js release ${{ needs.detect-release.outputs.release_type }} ${{ needs.detect-release.outputs.channel }} > /dev/null
- name: π₯ Download build artifacts
uses: actions/download-artifact@v4
with:
name: packages-${{ github.sha }}
path: ./artifacts/
- name: π Publish to PyPI
working-directory: packages/py
run: |
pip install hatch requests
# Get version from pyproject.toml
VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
echo "Current version: $VERSION"
# Check if version already exists on PyPI
if python -c "import requests; exit(0 if requests.get('https://pypi.org/pypi/glin-profanity/$VERSION/json').status_code == 200 else 1)" 2>/dev/null; then
echo "β οΈ Version $VERSION already exists on PyPI, skipping publish"
echo "This is expected if the version was already published in a previous run"
exit 0
fi
hatch build
# Publish to PyPI (supports pre-releases automatically)
hatch publish
env:
HATCH_INDEX_USER: __token__
HATCH_INDEX_AUTH: ${{ secrets.PYPI_TOKEN }}
# Create GitHub release
github-release:
name: π·οΈ GitHub Release
runs-on: ubuntu-latest
needs: [detect-release, bump-version, publish-npm, publish-pypi]
if: always() && needs.detect-release.outputs.should_release == 'true' && (needs.publish-npm.result == 'success' || needs.publish-pypi.result == 'success')
steps:
- name: π₯ Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: π₯ Download build artifacts
uses: actions/download-artifact@v4
with:
name: packages-${{ github.sha }}
path: ./artifacts/
- name: π·οΈ Create Git Tag
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
TAG_NAME="v${{ needs.bump-version.outputs.new_version }}"
# Check if tag already exists
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
echo "β οΈ Tag $TAG_NAME already exists, skipping tag creation"
else
git tag $TAG_NAME
git push origin $TAG_NAME
echo "β
Created and pushed tag $TAG_NAME"
fi
- name: π Generate Release Notes
id: notes
run: |
VERSION="${{ needs.bump-version.outputs.new_version }}"
CHANNEL="${{ needs.detect-release.outputs.channel }}"
# Determine release type emoji and description
if [ "$CHANNEL" = "alpha" ]; then
EMOJI="π±"
TYPE_DESC="Alpha Release"
elif [ "$CHANNEL" = "beta" ]; then
EMOJI="π§ͺ"
TYPE_DESC="Beta Release"
else
EMOJI="π"
TYPE_DESC="Stable Release"
fi
# Generate changelog from recent commits
CHANGELOG=$(git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD | grep -E "^- (feat|fix|docs|perf|refactor)" | head -10)
cat > release_notes.md << EOF
## $EMOJI $TYPE_DESC v$VERSION
### π¦ Installation
**JavaScript/TypeScript:**
\`\`\`bash
npm install glin-profanity@$CHANNEL
\`\`\`
**Python:**
\`\`\`bash
pip install glin-profanity$([ "$CHANNEL" != "stable" ] && echo " --pre" || echo "")
\`\`\`
### π Changes
$CHANGELOG
### π Package Information
- **npm tag**: \`$CHANNEL\`
- **PyPI classifier**: Development Status :: $([ "$CHANNEL" = "alpha" ] && echo "3 - Alpha" || [ "$CHANNEL" = "beta" ] && echo "4 - Beta" || echo "5 - Production/Stable")
- **Cross-platform**: Identical APIs for JavaScript and Python
### π Links
- [npm package](https://www.npmjs.com/package/glin-profanity)
- [PyPI package](https://pypi.org/project/glin-profanity/)
- [Documentation](https://github.com/GLINCKER/glin-profanity#readme)
---
π€ *This release was automatically generated*
EOF
- name: π Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.bump-version.outputs.new_version }}
name: v${{ needs.bump-version.outputs.new_version }}
body_path: release_notes.md
draft: false
prerelease: ${{ needs.detect-release.outputs.channel != 'stable' }}
files: ./artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Summary
summary:
name: π Release Summary
runs-on: ubuntu-latest
needs: [detect-release, bump-version, publish-npm, publish-pypi, github-release]
if: always() && needs.detect-release.outputs.should_release == 'true'
steps:
- name: β
Success Summary
if: needs.publish-npm.result == 'success' && needs.publish-pypi.result == 'success'
run: |
echo "## π Release Completed Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** v${{ needs.bump-version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "**Channel:** ${{ needs.detect-release.outputs.channel }}" >> $GITHUB_STEP_SUMMARY
echo "**Type:** ${{ needs.detect-release.outputs.release_type }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### π¦ Published Packages" >> $GITHUB_STEP_SUMMARY
echo "- β
npm: [glin-profanity@${{ needs.detect-release.outputs.channel }}](https://www.npmjs.com/package/glin-profanity)" >> $GITHUB_STEP_SUMMARY
echo "- β
PyPI: [glin-profanity](https://pypi.org/project/glin-profanity/)" >> $GITHUB_STEP_SUMMARY
echo "- β
GitHub: [Release v${{ needs.bump-version.outputs.new_version }}](https://github.com/GLINCKER/glin-profanity/releases/tag/v${{ needs.bump-version.outputs.new_version }})" >> $GITHUB_STEP_SUMMARY
- name: β οΈ Partial Success Summary
if: needs.publish-npm.result == 'success' || needs.publish-pypi.result == 'success'
run: |
echo "## β οΈ Release Partially Completed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** v${{ needs.bump-version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### π¦ Package Status" >> $GITHUB_STEP_SUMMARY
echo "- npm: ${{ needs.publish-npm.result == 'success' && 'β
' || 'β' }} ${{ needs.publish-npm.result }}" >> $GITHUB_STEP_SUMMARY
echo "- PyPI: ${{ needs.publish-pypi.result == 'success' && 'β
' || 'β' }} ${{ needs.publish-pypi.result }}" >> $GITHUB_STEP_SUMMARY
echo "- GitHub: ${{ needs.github-release.result == 'success' && 'β
' || 'β' }} ${{ needs.github-release.result }}" >> $GITHUB_STEP_SUMMARY
- name: β Failure Summary
if: needs.publish-npm.result != 'success' && needs.publish-pypi.result != 'success'
run: |
echo "## β Release Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Attempted Version:** v${{ needs.bump-version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### π Failure Details" >> $GITHUB_STEP_SUMMARY
echo "- npm: ${{ needs.publish-npm.result }}" >> $GITHUB_STEP_SUMMARY
echo "- PyPI: ${{ needs.publish-pypi.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please check the workflow logs and retry the release." >> $GITHUB_STEP_SUMMARY
exit 1