Skip to content

Commit 8f65a5e

Browse files
authored
Merge pull request #4 from miklosbagi/versioning
Add label-driven semver tagging and release automation
2 parents 25c58b0 + 486b2b3 commit 8f65a5e

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: PR version label
2+
3+
on:
4+
pull_request:
5+
types: [opened, labeled, unlabeled, synchronize]
6+
7+
permissions:
8+
pull-requests: write
9+
10+
jobs:
11+
validate-version-label:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Default to Tag/Patch on a brand-new PR
15+
if: github.event.action == 'opened'
16+
env:
17+
GH_TOKEN: ${{ github.token }}
18+
run: |
19+
labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name')
20+
if ! grep -qE '^Tag/(Patch|Minor|Major)$' <<< "$labels"; then
21+
gh pr edit "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --add-label "Tag/Patch"
22+
fi
23+
24+
- name: Exactly one Tag/* label, or none at all
25+
env:
26+
GH_TOKEN: ${{ github.token }}
27+
run: |
28+
# Re-read labels rather than trusting the event payload -- the
29+
# step above may have just changed them.
30+
labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name')
31+
count=$(grep -cE '^Tag/(Patch|Minor|Major)$' <<< "$labels" || true)
32+
echo "Tag/* labels present: $count"
33+
if [ "$count" -gt 1 ]; then
34+
echo "::error::More than one Tag/Patch, Tag/Minor, Tag/Major label is present -- pick exactly one, or remove all of them to skip versioning this PR."
35+
exit 1
36+
fi

‎.github/workflows/release.yml‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Version & Release
2+
3+
# Tagging/release happens on merge, reading whichever Tag/* label the PR
4+
# carried at that point -- not at label-add time. That's a deliberate
5+
# choice: tagging the PR branch's head commit as soon as a label is
6+
# added would (a) create an orphan tag if the PR is later closed
7+
# unmerged, and (b) point at a commit SHA that doesn't survive a squash
8+
# merge (main gets a brand-new commit). Using
9+
# `pull_request.merge_commit_sha` from the `closed`+merged event instead
10+
# is correct for merge, squash, and rebase alike, and never tags
11+
# anything that didn't actually land on main.
12+
13+
on:
14+
pull_request:
15+
types: [closed]
16+
17+
permissions:
18+
contents: write
19+
20+
jobs:
21+
tag-and-release:
22+
if: github.event.pull_request.merged == true
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
ref: main
29+
30+
- name: Determine bump level from the merged PR's labels
31+
id: bump
32+
env:
33+
GH_TOKEN: ${{ github.token }}
34+
run: |
35+
labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name')
36+
matches=$(grep -cE '^Tag/(Patch|Minor|Major)$' <<< "$labels" || true)
37+
38+
if [ "$matches" -eq 0 ]; then
39+
echo "No Tag/* label on this PR -- skipping version bump and release."
40+
echo "skip=true" >> "$GITHUB_OUTPUT"
41+
exit 0
42+
fi
43+
if [ "$matches" -gt 1 ]; then
44+
echo "::error::More than one Tag/* label on the merged PR -- the validate-version-label required check should have blocked this. Refusing to guess which one wins."
45+
exit 1
46+
fi
47+
48+
if grep -qxE 'Tag/Major' <<< "$labels"; then
49+
echo "level=major" >> "$GITHUB_OUTPUT"
50+
elif grep -qxE 'Tag/Minor' <<< "$labels"; then
51+
echo "level=minor" >> "$GITHUB_OUTPUT"
52+
else
53+
echo "level=patch" >> "$GITHUB_OUTPUT"
54+
fi
55+
echo "skip=false" >> "$GITHUB_OUTPUT"
56+
57+
- name: Compute next version
58+
if: steps.bump.outputs.skip != 'true'
59+
id: version
60+
run: |
61+
latest=$(git tag -l | { grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
62+
latest=${latest:-0.0.0}
63+
IFS='.' read -r major minor patch <<< "$latest"
64+
case "${{ steps.bump.outputs.level }}" in
65+
major) major=$((major + 1)); minor=0; patch=0 ;;
66+
minor) minor=$((minor + 1)); patch=0 ;;
67+
patch) patch=$((patch + 1)) ;;
68+
*) echo "::error::unknown bump level '${{ steps.bump.outputs.level }}'"; exit 1 ;;
69+
esac
70+
new="$major.$minor.$patch"
71+
echo "Bumping $latest -> $new (${{ steps.bump.outputs.level }})"
72+
echo "new_version=$new" >> "$GITHUB_OUTPUT"
73+
74+
- name: Create tag + release
75+
if: steps.bump.outputs.skip != 'true'
76+
env:
77+
GH_TOKEN: ${{ github.token }}
78+
run: |
79+
gh release create "${{ steps.version.outputs.new_version }}" \
80+
--repo "${{ github.repository }}" \
81+
--target "${{ github.event.pull_request.merge_commit_sha }}" \
82+
--title "${{ steps.version.outputs.new_version }}" \
83+
--generate-notes

0 commit comments

Comments
 (0)