Skip to content

Commit 10882aa

Browse files
ci: add bumpversion github workflow
1 parent 60e113c commit 10882aa

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed

.github/workflows/bumpversion.yml

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# GitHub action to bump version, update changelog, and create release.
2+
# Can optionally be executed as a two-stage process with an intermediate pull request to allow reviews.
3+
4+
name: Bump Version
5+
run-name: Bump version (${{ inputs.bump-type }}) by @${{ github.actor }}
6+
7+
# Note: Enable GitHub Actions to create pull requests in repository settings:
8+
# Settings -> Actions -> General -> Workflow permissions -> Allow GitHub Actions to create and approve pull requests
9+
#
10+
# Optional secrets:
11+
# - GPG_PRIVATE_KEY: The private GPG key for signing commits and tags
12+
# - GPG_PASSPHRASE: The passphrase for the GPG private key (optional if key has no passphrase)
13+
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
on:
19+
workflow_dispatch:
20+
inputs:
21+
bump-type:
22+
description: 'Version bump type'
23+
required: true
24+
default: 'patch'
25+
type: choice
26+
options:
27+
- major
28+
- minor
29+
- patch
30+
- post-review-release # Do not bump version, just create tag & release based on latest changelog
31+
fail-on-empty-changelog:
32+
description: 'Fail if changelog is empty'
33+
required: false
34+
default: true
35+
type: boolean
36+
create-pull-request:
37+
description: 'Create pull request for review'
38+
required: false
39+
default: true
40+
type: boolean
41+
gpg-signing:
42+
description: 'Sign commits and tags with GPG'
43+
required: false
44+
default: true
45+
type: boolean
46+
47+
jobs:
48+
bump_version:
49+
runs-on: ubuntu-latest
50+
51+
outputs:
52+
version: ${{ steps.bump.outputs.current-version }}
53+
pr-number: ${{ steps.create_pr.outputs.pull-request-number }}
54+
55+
steps:
56+
- name: Checkout repository
57+
uses: actions/checkout@v5
58+
with:
59+
token: ${{ secrets.GITHUB_TOKEN }}
60+
61+
- name: Setup Python environment
62+
uses: astral-sh/setup-uv@v6
63+
64+
- name: Install bump-my-version
65+
run: uv tool install bump-my-version
66+
67+
- name: Bump version
68+
id: bump
69+
shell: bash
70+
run: |
71+
if [[ "${{ inputs.bump-type }}" == "post-review-release" ]]; then
72+
echo "::notice::Post-review release: skipping version bump"
73+
else
74+
echo "::notice::Bumping version with type: ${{ inputs.bump-type }}"
75+
bump-my-version bump ${{ inputs.bump-type }}
76+
fi
77+
78+
CURRENT_VERSION=$(bump-my-version show current_version)
79+
echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
80+
echo "::notice::Current version: $CURRENT_VERSION"
81+
82+
- name: Update changelog
83+
id: changelog
84+
uses: release-flow/keep-a-changelog-action@v2
85+
if: ${{ inputs.bump-type != 'post-review-release' }}
86+
with:
87+
command: bump
88+
version: ${{ inputs.bump-type }}
89+
keep-unreleased-section: true
90+
fail-on-empty-release-notes: ${{ inputs.fail-on-empty-changelog }}
91+
92+
- name: Query changelog for release notes
93+
id: query_changelog
94+
uses: release-flow/keep-a-changelog-action@v2
95+
with:
96+
command: query
97+
version: latest
98+
99+
- name: Configure Git and GPG
100+
id: git_setup
101+
run: |
102+
# Configure Git user
103+
git config --global user.name "${{ github.actor }}"
104+
git config --global user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
105+
106+
if ${{ inputs.gpg-signing }}; then
107+
echo "::group::Setting up GPG signing"
108+
109+
# Validate required secrets
110+
if [ -z "${{ secrets.GPG_PRIVATE_KEY }}" ]; then
111+
echo "::error::GPG_PRIVATE_KEY secret is required when GPG signing is enabled"
112+
exit 1
113+
fi
114+
115+
# Create GPG directory with proper permissions
116+
mkdir -p ~/.gnupg
117+
chmod 700 ~/.gnupg
118+
119+
# Configure GPG for non-interactive use
120+
cat > ~/.gnupg/gpg.conf << EOF
121+
use-agent
122+
pinentry-mode loopback
123+
batch
124+
no-tty
125+
EOF
126+
127+
# Configure GPG agent for GitHub Actions
128+
cat > ~/.gnupg/gpg-agent.conf << EOF
129+
allow-preset-passphrase
130+
default-cache-ttl 21600
131+
max-cache-ttl 21600
132+
pinentry-program /usr/bin/pinentry-curses
133+
EOF
134+
135+
# Set proper permissions
136+
chmod 600 ~/.gnupg/gpg.conf ~/.gnupg/gpg-agent.conf
137+
138+
# Kill any existing GPG agent and start fresh
139+
gpgconf --kill gpg-agent 2>/dev/null || true
140+
gpg-agent --daemon --allow-preset-passphrase 2>/dev/null || true
141+
142+
# Import the GPG private key
143+
echo "Importing GPG private key..."
144+
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg \
145+
--pinentry-mode loopback \
146+
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
147+
--import --batch --yes
148+
149+
# Extract GPG key ID
150+
KEY_ID=$(echo "${{ secrets.GPG_PRIVATE_KEY }}" | \
151+
gpg --with-colons --import-options show-only --import 2>/dev/null | \
152+
awk -F: '/^sec:/ {print $5; exit}')
153+
154+
if [ -z "$KEY_ID" ]; then
155+
echo "::error::Failed to extract GPG key ID from private key"
156+
exit 1
157+
fi
158+
159+
echo "Using GPG key ID: $KEY_ID"
160+
161+
# Handle passphrase preset if provided
162+
if [ -n "${{ secrets.GPG_PASSPHRASE }}" ]; then
163+
echo "Setting up GPG passphrase preset..."
164+
# Extract keygrip
165+
GPG_KEYGRIP=$(gpg --with-keygrip --list-secret-keys "$KEY_ID" 2>/dev/null | \
166+
grep -A1 "^sec" | grep "Keygrip" | head -1 | \
167+
sed 's/.*Keygrip = //' | tr -d ' ')
168+
169+
if [ -n "$GPG_KEYGRIP" ]; then
170+
echo "Found keygrip: $GPG_KEYGRIP"
171+
172+
# Convert passphrase to hex format
173+
GPG_PASSPHRASE_HEX=$(echo -n "${{ secrets.GPG_PASSPHRASE }}" | \
174+
od -A n -t x1 | tr -d ' \n')
175+
176+
# Preset the passphrase in GPG agent
177+
echo "PRESET_PASSPHRASE $GPG_KEYGRIP -1 $GPG_PASSPHRASE_HEX" | \
178+
gpg-connect-agent --no-autostart
179+
180+
echo "Passphrase preset successful for keygrip: $GPG_KEYGRIP"
181+
else
182+
echo "::warning::Could not extract keygrip, passphrase preset may not work"
183+
fi
184+
else
185+
echo "No GPG passphrase provided, assuming key has no passphrase"
186+
fi
187+
188+
# Test GPG signing capability
189+
echo "Testing GPG signing..."
190+
if echo "test signing" | gpg \
191+
--pinentry-mode loopback \
192+
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
193+
--clearsign --default-key "$KEY_ID" >/dev/null 2>&1; then
194+
echo "::notice::GPG signing test successful"
195+
else
196+
echo "::error::GPG signing test failed"
197+
exit 1
198+
fi
199+
200+
# Configure Git for GPG signing
201+
git config --global commit.gpgSign true
202+
git config --global tag.gpgSign true
203+
git config --global user.signingkey "$KEY_ID"
204+
git config --global gpg.program gpg
205+
206+
echo "::notice::GPG signing enabled with key: $KEY_ID"
207+
echo "::endgroup::"
208+
else
209+
echo "::notice::GPG signing disabled"
210+
git config --global commit.gpgSign false
211+
git config --global tag.gpgSign false
212+
fi
213+
214+
- name: Create version commit
215+
id: create_commit
216+
if: ${{ inputs.bump-type != 'post-review-release' }}
217+
run: |
218+
git add .
219+
git commit -m "docs: Increase version to ${{ steps.bump.outputs.current-version }}"
220+
if ${{ inputs.create-pull-request }}; then
221+
echo "::notice::Commit created, will be pushed via pull request"
222+
else
223+
echo "::notice::Pushing commit directly"
224+
git push
225+
fi
226+
227+
- name: Create Pull Request
228+
id: create_pr
229+
if: ${{ inputs.create-pull-request && inputs.bump-type != 'post-review-release' }}
230+
uses: peter-evans/create-pull-request@v7
231+
with:
232+
title: "Bump version to ${{ steps.bump.outputs.current-version }}"
233+
body: |
234+
## Version Bump: ${{ steps.bump.outputs.current-version }}
235+
236+
This PR bumps the version to `${{ steps.bump.outputs.current-version }}` and updates the changelog.
237+
238+
### Next Steps
239+
After merging this PR, trigger this workflow again with `bump type` set to `post-review-release` to create the tag and GitHub release.
240+
241+
### Changelog (automatically extracted from Unreleased section)
242+
${{ steps.query_changelog.outputs.release-notes }}
243+
244+
---
245+
*This PR was automatically created by the Bump Version workflow*
246+
branch: bumpversion-${{ steps.bump.outputs.current-version }}
247+
token: ${{ secrets.GITHUB_TOKEN }}
248+
delete-branch: true
249+
250+
- name: Create version tag
251+
id: create_tag
252+
if: ${{ !inputs.create-pull-request || inputs.bump-type == 'post-review-release' }}
253+
run: |
254+
TAG_NAME="v${{ steps.bump.outputs.current-version }}"
255+
echo "::notice::Creating tag: $TAG_NAME"
256+
257+
git tag "$TAG_NAME" -m "Version ${{ steps.bump.outputs.current-version }}"
258+
git push origin "$TAG_NAME"
259+
260+
echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT
261+
262+
- name: Create GitHub Release
263+
id: create_release
264+
if: ${{ !inputs.create-pull-request || inputs.bump-type == 'post-review-release' }}
265+
uses: ncipollo/release-action@v1
266+
with:
267+
tag: v${{ steps.bump.outputs.current-version }}
268+
name: v${{ steps.bump.outputs.current-version }}
269+
body: ${{ steps.query_changelog.outputs.release-notes }}
270+
draft: false
271+
prerelease: false
272+
token: ${{ secrets.GITHUB_TOKEN }}
273+
makeLatest: true
274+
275+
- name: Summary
276+
if: always()
277+
run: |
278+
echo "## Workflow Summary" >> $GITHUB_STEP_SUMMARY
279+
echo "- **Bump Type:** ${{ inputs.bump-type }}" >> $GITHUB_STEP_SUMMARY
280+
echo "- **Version:** ${{ steps.bump.outputs.current-version }}" >> $GITHUB_STEP_SUMMARY
281+
echo "- **GPG Signing:** ${{ inputs.gpg-signing }}" >> $GITHUB_STEP_SUMMARY
282+
283+
if [[ "${{ steps.create_pr.outputs.pull-request-number }}" != "" ]]; then
284+
PR_NUMBER="${{ steps.create_pr.outputs.pull-request-number }}"
285+
PR_URL="https://github.com/${{ github.repository }}/pull/$PR_NUMBER"
286+
echo "- **Pull Request:** [#$PR_NUMBER]($PR_URL)" >> $GITHUB_STEP_SUMMARY
287+
echo "" >> $GITHUB_STEP_SUMMARY
288+
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
289+
echo "1. Review and merge [Pull Request #$PR_NUMBER]($PR_URL)" >> $GITHUB_STEP_SUMMARY
290+
echo "2. After merging, re-run this workflow with \`bump type\` set to \`post-review-release\` to create the tag and GitHub release" >> $GITHUB_STEP_SUMMARY
291+
fi
292+
293+
if [[ "${{ steps.create_release.outputs.html_url }}" != "" ]]; then
294+
RELEASE_URL="${{ steps.create_release.outputs.html_url }}"
295+
echo "- **GitHub Release:** [Version ${{ steps.bump.outputs.current-version }}]($RELEASE_URL)" >> $GITHUB_STEP_SUMMARY
296+
fi
297+
298+
echo "" >> $GITHUB_STEP_SUMMARY
299+
echo "### 📋 Workflow Details" >> $GITHUB_STEP_SUMMARY
300+
echo "- **Triggered by:** @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
301+
echo "- **Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
302+
echo "- **Run ID:** [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)