Skip to content

Commit 18897bf

Browse files
groksrcclaude
andcommitted
ci(release): auto-bump version, tag, and publish from main
Replace the tag-triggered release workflow with a workflow_dispatch path that runs against main. Reads __version__ from __init__.py, accepts patch|minor|major or explicit semver as input, updates both __init__.py and plugin.yaml, commits as chore(release): vX.Y.Z, tags, pushes both, and creates a GitHub Release. Pulls the matching CHANGELOG.md section as the release body when present; falls back to auto-generated notes otherwise. Mirrors the openclaw-basic-memory release pattern, adapted for our two-file Python version sources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b1118c commit 18897bf

1 file changed

Lines changed: 122 additions & 20 deletions

File tree

.github/workflows/release.yml

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,149 @@
11
name: 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.
515
on:
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

830
jobs:
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

Comments
 (0)