Skip to content

Commit 23bbeb6

Browse files
Merge pull request #12 from seranking/release-dispatch-bump
ci: manual release dispatch with patch/minor/major bump
2 parents 51af010 + d435502 commit 23bbeb6

1 file changed

Lines changed: 96 additions & 10 deletions

File tree

.github/workflows/release.yml

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,124 @@
11
name: Release
22

3-
# Triggered by pushing a vX.Y.Z tag. Turns the tag into a GitHub Release
4-
# deterministically — asserts the tag matches the manifest versions, pulls the
5-
# matching CHANGELOG section for the notes, and creates (or updates) the Release.
6-
# Removes the manual `gh release create` step and the tag-without-release drift
7-
# it caused. workflow_dispatch lets you (re)build a Release for an existing tag.
3+
# Two ways to cut a release:
4+
# 1. Push a signed vX.Y.Z tag locally → a GitHub-Verified release. This is the
5+
# `push: tags` trigger below. Use it when you want the Verified badge (the
6+
# Actions runner cannot sign with your key).
7+
# 2. Run this workflow from the Actions UI ("Run workflow") and pick a
8+
# patch / minor / major bump (or type an explicit version). The `bump` job
9+
# rewrites the version across all four manifests, commits `release: vX.Y.Z`
10+
# to main as github-actions[bot], and pushes the tag. Convenient and
11+
# button-only — but the tag is created by Actions, so it is NOT Verified.
12+
#
13+
# Either way the `release` job turns the tag into a GitHub Release: it asserts
14+
# the tag matches all four manifest versions and uses the matching CHANGELOG
15+
# section as the notes (falling back to auto-generated notes when none exists).
816

917
on:
1018
push:
1119
tags: ['v*']
1220
workflow_dispatch:
1321
inputs:
14-
tag:
15-
description: 'Existing vX.Y.Z tag to (re)build a Release for'
16-
required: true
22+
release_type:
23+
description: "Semver bump (ignored if 'version' is set)."
24+
type: choice
25+
options: [patch, minor, major]
26+
default: patch
27+
version:
28+
description: "Explicit version override, e.g. 2.11.0 (optional)."
29+
required: false
30+
default: ""
1731

1832
permissions:
1933
contents: write
2034

35+
concurrency:
36+
group: release
37+
cancel-in-progress: false
38+
2139
jobs:
40+
# Manual dispatch only: bump the four manifests, commit to main, push the tag.
41+
# A tag pushed with GITHUB_TOKEN does not re-trigger this workflow, so the
42+
# release job below runs in the same dispatch run via `needs`.
43+
bump:
44+
if: github.event_name == 'workflow_dispatch'
45+
runs-on: ubuntu-latest
46+
outputs:
47+
tag: ${{ steps.bump.outputs.tag }}
48+
steps:
49+
- uses: actions/checkout@v5
50+
with:
51+
# Default GITHUB_TOKEN (contents: write) pushes the bump commit + tag;
52+
# main is unprotected, so no PAT is needed.
53+
ref: main
54+
fetch-depth: 0
55+
56+
- id: bump
57+
env:
58+
RELEASE_TYPE: ${{ inputs.release_type }}
59+
VERSION_OVERRIDE: ${{ inputs.version }}
60+
shell: bash
61+
run: |
62+
set -euo pipefail
63+
CUR="$(jq -r '.version' .claude-plugin/plugin.json)"
64+
if [ -n "$VERSION_OVERRIDE" ]; then
65+
NEW="$VERSION_OVERRIDE"
66+
else
67+
IFS=. read -r MA MI PA <<< "$CUR"
68+
case "$RELEASE_TYPE" in
69+
major) MA=$((MA + 1)); MI=0; PA=0 ;;
70+
minor) MI=$((MI + 1)); PA=0 ;;
71+
patch) PA=$((PA + 1)) ;;
72+
*) echo "::error::unknown release_type '$RELEASE_TYPE'"; exit 1 ;;
73+
esac
74+
NEW="$MA.$MI.$PA"
75+
fi
76+
if ! printf '%s' "$NEW" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
77+
echo "::error::computed version '$NEW' is not X.Y.Z"; exit 1
78+
fi
79+
echo "bumping $CUR -> $NEW ($RELEASE_TYPE)"
80+
81+
tmp="$(mktemp)"
82+
jq --arg v "$NEW" '.version = $v' .claude-plugin/plugin.json > "$tmp" && mv "$tmp" .claude-plugin/plugin.json
83+
jq --arg v "$NEW" '.metadata.version = $v | .plugins[0].version = $v' .claude-plugin/marketplace.json > "$tmp" && mv "$tmp" .claude-plugin/marketplace.json
84+
jq --arg v "$NEW" '.version = $v' .cursor-plugin/plugin.json > "$tmp" && mv "$tmp" .cursor-plugin/plugin.json
85+
86+
# Sanity: every version field must read $NEW before we tag.
87+
m=$(jq -r '.metadata.version' .claude-plugin/marketplace.json)
88+
e=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)
89+
p=$(jq -r '.version' .claude-plugin/plugin.json)
90+
c=$(jq -r '.version' .cursor-plugin/plugin.json)
91+
if [ "$m" != "$NEW" ] || [ "$e" != "$NEW" ] || [ "$p" != "$NEW" ] || [ "$c" != "$NEW" ]; then
92+
echo "::error::manifests did not all update to $NEW (metadata=$m entry=$e plugin=$p cursor=$c)"; exit 1
93+
fi
94+
95+
git config user.name "github-actions[bot]"
96+
git config user.email "github-actions[bot]@users.noreply.github.com"
97+
git commit -am "release: v$NEW"
98+
git push origin main
99+
git tag -a "v$NEW" -m "v$NEW"
100+
git push origin "v$NEW"
101+
echo "tag=v$NEW" >> "$GITHUB_OUTPUT"
102+
22103
release:
104+
needs: [bump]
105+
# Runs for a pushed tag (push event) OR right after a successful bump
106+
# (dispatch). always() is required so a skipped bump (the push path) does
107+
# not skip this job.
108+
if: ${{ always() && (github.event_name == 'push' || needs.bump.result == 'success') }}
23109
runs-on: ubuntu-latest
24110
steps:
25111
- name: Resolve tag
26112
id: tag
27113
run: |
28114
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
29-
echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
115+
echo "name=${{ needs.bump.outputs.tag }}" >> "$GITHUB_OUTPUT"
30116
else
31117
echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
32118
fi
33119
34120
- name: Check out the tagged commit
35-
uses: actions/checkout@v4
121+
uses: actions/checkout@v5
36122
with:
37123
ref: ${{ steps.tag.outputs.name }}
38124
fetch-depth: 0

0 commit comments

Comments
 (0)