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: release | |
| # Manual trigger: Actions → release → Run workflow. Always runs against main | |
| # (the workflow validates GITHUB_REF). Steps: | |
| # 1. Compute the new version from the `version` input (patch/minor/major | |
| # or explicit semver). The current version is read from __init__.py. | |
| # 2. Update __version__ in __init__.py and version in plugin.yaml. | |
| # 3. Commit as `chore(release): vX.Y.Z`, tag, push to main + push the tag. | |
| # 4. Publish a GitHub Release. Body is the matching `## [X.Y.Z]` block from | |
| # CHANGELOG.md when present; otherwise auto-generated release notes. | |
| # | |
| # Recommended flow: land a PR that adds a `## [X.Y.Z]` section to CHANGELOG.md | |
| # first, then run this workflow with the matching version so the release notes | |
| # are the hand-written changelog instead of commit-message-derived notes. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version bump (`patch`, `minor`, `major`) or explicit semver (`0.3.0`)" | |
| required: true | |
| default: "patch" | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| name: Tag and Publish GitHub Release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate trigger is main | |
| run: | | |
| if [ "$GITHUB_REF" != "refs/heads/main" ]; then | |
| echo "::error::release must run against main. Got $GITHUB_REF" | |
| exit 1 | |
| fi | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Compute new version | |
| id: bump | |
| run: | | |
| set -euo pipefail | |
| VERSION_INPUT="${{ github.event.inputs.version }}" | |
| # Source of truth for the current version is __init__.py. | |
| CURRENT=$(grep -E '^__version__ = ' __init__.py \ | |
| | sed -E 's/^__version__ = "([^"]+)".*/\1/') | |
| if [ -z "$CURRENT" ]; then | |
| echo "::error::Could not read __version__ from __init__.py" | |
| exit 1 | |
| fi | |
| echo "Current version: $CURRENT" | |
| if [[ "$VERSION_INPUT" =~ ^(patch|minor|major)$ ]]; then | |
| IFS=. read -r MAJ MIN PAT <<< "$CURRENT" | |
| case "$VERSION_INPUT" in | |
| major) MAJ=$((MAJ + 1)); MIN=0; PAT=0 ;; | |
| minor) MIN=$((MIN + 1)); PAT=0 ;; | |
| patch) PAT=$((PAT + 1)) ;; | |
| esac | |
| NEW="${MAJ}.${MIN}.${PAT}" | |
| elif [[ "$VERSION_INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| NEW="$VERSION_INPUT" | |
| else | |
| echo "::error::Invalid version input: '$VERSION_INPUT'" | |
| echo "::error::Use patch|minor|major or explicit X.Y.Z" | |
| exit 1 | |
| fi | |
| if [ "$NEW" = "$CURRENT" ]; then | |
| echo "::error::New version equals current ($NEW). Pick a different version." | |
| exit 1 | |
| fi | |
| echo "New version: $NEW" | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "version=$NEW" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$NEW" >> "$GITHUB_OUTPUT" | |
| - name: Refuse if tag already exists | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ steps.bump.outputs.tag }}" | |
| if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then | |
| echo "::error::Tag $TAG already exists locally. Pick a different version." | |
| exit 1 | |
| fi | |
| if git ls-remote --tags origin "$TAG" | grep -q "refs/tags/$TAG$"; then | |
| echo "::error::Tag $TAG already exists on origin. Pick a different version." | |
| exit 1 | |
| fi | |
| - name: Update version files | |
| run: | | |
| set -euo pipefail | |
| NEW="${{ steps.bump.outputs.version }}" | |
| sed -i -E "s/^__version__ = \"[^\"]+\"/__version__ = \"${NEW}\"/" __init__.py | |
| sed -i -E "s/^version: .*/version: ${NEW}/" plugin.yaml | |
| # Verify both files changed and that the new version is present. | |
| grep -q "^__version__ = \"${NEW}\"" __init__.py | |
| grep -q "^version: ${NEW}$" plugin.yaml | |
| echo "--- diff ---" | |
| git --no-pager diff -- __init__.py plugin.yaml | |
| - name: Configure Git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Commit and tag | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ steps.bump.outputs.tag }}" | |
| git add __init__.py plugin.yaml | |
| git commit -m "chore(release): ${TAG}" | |
| git tag -a "${TAG}" -m "${TAG}" | |
| - name: Push commit and tag | |
| run: | | |
| set -euo pipefail | |
| git push origin HEAD:main | |
| git push origin "${{ steps.bump.outputs.tag }}" | |
| - name: Extract CHANGELOG section for this version | |
| id: changelog | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ steps.bump.outputs.version }}" | |
| # Pull lines between `## [VERSION]` and the next `## [` heading. | |
| SECTION=$(awk -v ver="$VERSION" ' | |
| $0 ~ "^## \\[" ver "\\]" { found=1; next } | |
| found && /^## \[/ { exit } | |
| found { print } | |
| ' CHANGELOG.md) | |
| if [ -z "$SECTION" ]; then | |
| echo "::warning::No CHANGELOG.md section found for v${VERSION} — falling back to auto-generated release notes." | |
| echo "has_section=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_section=true" >> "$GITHUB_OUTPUT" | |
| { | |
| echo 'body<<EOF_CHANGELOG' | |
| echo "$SECTION" | |
| echo 'EOF_CHANGELOG' | |
| } >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.bump.outputs.tag }} | |
| name: ${{ steps.bump.outputs.tag }} | |
| body: ${{ steps.changelog.outputs.body }} | |
| generate_release_notes: ${{ steps.changelog.outputs.has_section == 'false' }} | |
| token: ${{ secrets.GITHUB_TOKEN }} |