11name : release
22
3- # Manual only. Trigger from Actions → release → Run workflow,
4- # selecting a v*.*.* tag from the ref dropdown.
3+ # Manual trigger: Actions → release → Run workflow. Always runs against main
4+ # (the workflow validates GITHUB_REF). Steps:
5+ # 1. Compute the new version from the `version` input (patch/minor/major
6+ # or explicit semver). The current version is read from __init__.py.
7+ # 2. Update __version__ in __init__.py and version in plugin.yaml.
8+ # 3. Commit as `chore(release): vX.Y.Z`, tag, push to main + push the tag.
9+ # 4. Publish a GitHub Release. Body is the matching `## [X.Y.Z]` block from
10+ # CHANGELOG.md when present; otherwise auto-generated release notes.
11+ #
12+ # Recommended flow: land a PR that adds a `## [X.Y.Z]` section to CHANGELOG.md
13+ # first, then run this workflow with the matching version so the release notes
14+ # are the hand-written changelog instead of commit-message-derived notes.
515on :
616 workflow_dispatch :
17+ inputs :
18+ version :
19+ description : " Version bump (`patch`, `minor`, `major`) or explicit semver (`0.3.0`)"
20+ required : true
21+ default : " patch"
22+
23+ permissions :
24+ contents : write
25+
26+ concurrency :
27+ group : release-${{ github.ref }}
28+ cancel-in-progress : false
729
830jobs :
931 release :
10- name : Publish GitHub Release
32+ name : Tag and Publish GitHub Release
1133 runs-on : ubuntu-latest
12- permissions :
13- contents : write
1434 steps :
15- - name : Validate ref is a v*.*.* tag
35+ - name : Validate trigger is main
1636 run : |
17- if [ "${GITHUB_REF_TYPE}" != "tag" ]; then
18- echo "::error::This workflow must run against a tag. Got ref_type=$GITHUB_REF_TYPE ref=$GITHUB_REF"
19- exit 1
20- fi
21- if [[ ! "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
22- echo "::error::Tag must match v X.Y.Z format. Got: $GITHUB_REF_NAME"
37+ if [ "$GITHUB_REF" != "refs/heads/main" ]; then
38+ echo "::error::release must run against main. Got $GITHUB_REF"
2339 exit 1
2440 fi
2541
2642 - uses : actions/checkout@v4
2743 with :
28- ref : ${{ github.ref }}
2944 fetch-depth : 0
45+ token : ${{ secrets.GITHUB_TOKEN }}
46+
47+ - name : Compute new version
48+ id : bump
49+ run : |
50+ set -euo pipefail
51+ VERSION_INPUT="${{ github.event.inputs.version }}"
52+
53+ # Source of truth for the current version is __init__.py.
54+ CURRENT=$(grep -E '^__version__ = ' __init__.py \
55+ | sed -E 's/^__version__ = "([^"]+)".*/\1/')
56+ if [ -z "$CURRENT" ]; then
57+ echo "::error::Could not read __version__ from __init__.py"
58+ exit 1
59+ fi
60+ echo "Current version: $CURRENT"
61+
62+ if [[ "$VERSION_INPUT" =~ ^(patch|minor|major)$ ]]; then
63+ IFS=. read -r MAJ MIN PAT <<< "$CURRENT"
64+ case "$VERSION_INPUT" in
65+ major) MAJ=$((MAJ + 1)); MIN=0; PAT=0 ;;
66+ minor) MIN=$((MIN + 1)); PAT=0 ;;
67+ patch) PAT=$((PAT + 1)) ;;
68+ esac
69+ NEW="${MAJ}.${MIN}.${PAT}"
70+ elif [[ "$VERSION_INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
71+ NEW="$VERSION_INPUT"
72+ else
73+ echo "::error::Invalid version input: '$VERSION_INPUT'"
74+ echo "::error::Use patch|minor|major or explicit X.Y.Z"
75+ exit 1
76+ fi
77+
78+ if [ "$NEW" = "$CURRENT" ]; then
79+ echo "::error::New version equals current ($NEW). Pick a different version."
80+ exit 1
81+ fi
82+
83+ echo "New version: $NEW"
84+ echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
85+ echo "version=$NEW" >> "$GITHUB_OUTPUT"
86+ echo "tag=v$NEW" >> "$GITHUB_OUTPUT"
87+
88+ - name : Refuse if tag already exists
89+ run : |
90+ set -euo pipefail
91+ TAG="${{ steps.bump.outputs.tag }}"
92+ if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then
93+ echo "::error::Tag $TAG already exists locally. Pick a different version."
94+ exit 1
95+ fi
96+ if git ls-remote --tags origin "$TAG" | grep -q "refs/tags/$TAG$"; then
97+ echo "::error::Tag $TAG already exists on origin. Pick a different version."
98+ exit 1
99+ fi
100+
101+ - name : Update version files
102+ run : |
103+ set -euo pipefail
104+ NEW="${{ steps.bump.outputs.version }}"
105+ sed -i -E "s/^__version__ = \"[^\"]+\"/__version__ = \"${NEW}\"/" __init__.py
106+ sed -i -E "s/^version: .*/version: ${NEW}/" plugin.yaml
107+
108+ # Verify both files changed and that the new version is present.
109+ grep -q "^__version__ = \"${NEW}\"" __init__.py
110+ grep -q "^version: ${NEW}$" plugin.yaml
111+
112+ echo "--- diff ---"
113+ git --no-pager diff -- __init__.py plugin.yaml
114+
115+ - name : Configure Git identity
116+ run : |
117+ git config user.name "github-actions[bot]"
118+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
119+
120+ - name : Commit and tag
121+ run : |
122+ set -euo pipefail
123+ TAG="${{ steps.bump.outputs.tag }}"
124+ git add __init__.py plugin.yaml
125+ git commit -m "chore(release): ${TAG}"
126+ git tag -a "${TAG}" -m "${TAG}"
127+
128+ - name : Push commit and tag
129+ run : |
130+ set -euo pipefail
131+ git push origin HEAD:main
132+ git push origin "${{ steps.bump.outputs.tag }}"
30133
31- - name : Extract CHANGELOG section for this tag
134+ - name : Extract CHANGELOG section for this version
32135 id : changelog
33136 run : |
34- VERSION="${GITHUB_REF_NAME#v}"
137+ set -euo pipefail
138+ VERSION="${{ steps.bump.outputs.version }}"
35139 # Pull lines between `## [VERSION]` and the next `## [` heading.
36140 SECTION=$(awk -v ver="$VERSION" '
37141 $0 ~ "^## \\[" ver "\\]" { found=1; next }
38142 found && /^## \[/ { exit }
39143 found { print }
40144 ' CHANGELOG.md)
41145 if [ -z "$SECTION" ]; then
42- echo "::warning::No CHANGELOG.md section found for v$VERSION — falling back to auto-generated release notes."
146+ echo "::warning::No CHANGELOG.md section found for v${ VERSION} — falling back to auto-generated release notes."
43147 echo "has_section=false" >> "$GITHUB_OUTPUT"
44148 else
45149 echo "has_section=true" >> "$GITHUB_OUTPUT"
@@ -53,10 +157,8 @@ jobs:
53157 - name : Create GitHub Release
54158 uses : softprops/action-gh-release@v2
55159 with :
56- tag_name : ${{ github.ref_name }}
57- name : ${{ github.ref_name }}
160+ tag_name : ${{ steps.bump.outputs.tag }}
161+ name : ${{ steps.bump.outputs.tag }}
58162 body : ${{ steps.changelog.outputs.body }}
59- # If we couldn't find a CHANGELOG section, let GitHub auto-generate
60- # release notes from commit messages instead of publishing an empty body.
61163 generate_release_notes : ${{ steps.changelog.outputs.has_section == 'false' }}
62164 token : ${{ secrets.GITHUB_TOKEN }}
0 commit comments