Skip to content

Commit feab60d

Browse files
ritwik-gclaude
andauthored
[MISC] Add 'auto' version bump to OSS create-release (#2136)
* [MISC] Add 'auto' version bump to OSS create-release Adds an 'auto' choice (now the default) to create-release.yaml that picks the OSS version bump from merged PR titles: a [FEAT]/[GATED-FEAT] PR merged since the last release -> minor, otherwise patch. These are the only feature PR types in the contribution guide, so this matches the documented SemVer intent without any new labeling. Details: - 'auto' resolves in a new "Resolve auto bump" step (main mode only; hotfix lines stay patch-only). Fail-safe is always patch, so a compare/PR query hiccup never over-bumps the public version. - Hotfix-mode input validation now accepts 'auto' (maps to patch). - compute-version consumes the resolved bump; dry-run/final summaries show "auto -> minor/patch" for an explicit audit trail. - 'auto' never selects major — that stays behind the confirm_major gate. Validated by replaying the classifier over the last 13 real releases: 12/13 matched the human bump; the lone diff was a discretionary minor with no FEAT PR (recoverable via manual minor override, which the notice points to). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * [MISC] Address review: PR-read perms, patch fallback, truncation warning - Add `pull-requests: read` to the job (the permissions block sets unlisted scopes to none, so `gh pr list` would 403 and 'auto' would silently always fall back to patch). [CodeRabbit] - BUMP_TYPE falls back to 'patch' instead of 'auto' when resolved is empty, avoiding a latent "Unknown bump type: 'auto'" job failure. [Greptile] - Surface a warning when the merged-PR query hits the 200-result cap instead of silently under-counting FEAT PRs. [Greptile] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * [MISC] Harden auto-bump resolve step (review follow-ups) From a multi-agent review of the auto-bump step: - Guard the jq parses: malformed/non-array stdout now degrades to patch instead of crashing the job under set -e/pipefail (upholds the "never fail the release outright" invariant). Also validates TOTAL/FEAT_COUNT are numeric before arithmetic. - Detect gh failure by exit status (if ! PR_JSON=$(...)) and surface captured stderr, so a permanent 403 (e.g. dropped pull-requests scope) is diagnosable rather than a cause-free warning that silently patches forever. - Add '// empty' to the published_at lookup for consistency with get-latest, so a JSON null can't leak through as the literal "null". - Only emit the truncation warning when no FEAT was found within the cap (the only case where truncation could change the outcome). - Fix an inaccurate comment ("silently" -> "with a warning") and reword the self-referential permissions comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 076fd6f commit feab60d

1 file changed

Lines changed: 87 additions & 6 deletions

File tree

.github/workflows/create-release.yaml

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ on:
44
workflow_dispatch:
55
inputs:
66
version_bump:
7-
description: "Version bump type (hotfix branches force 'patch')"
7+
description: "Version bump ('auto' = minor if a FEAT/GATED-FEAT PR merged since the last release, else patch; hotfix branches force 'patch')"
88
required: true
9-
default: "patch"
9+
default: "auto"
1010
type: choice
1111
options:
12+
- auto
1213
- patch
1314
- minor
1415
- major
@@ -39,6 +40,11 @@ jobs:
3940
runs-on: ubuntu-latest
4041
permissions:
4142
contents: write
43+
# 'auto' version_bump classifies merged PR titles via `gh pr list`, which
44+
# needs read access to pull requests. Declaring an explicit permissions
45+
# block sets every unlisted scope to 'none', so without this the query 403s
46+
# and 'auto' falls back to patch (with a warning) on every run.
47+
pull-requests: read
4248
defaults:
4349
run:
4450
shell: bash -euo pipefail {0}
@@ -82,7 +88,9 @@ jobs:
8288
env:
8389
BUMP: ${{ github.event.inputs.version_bump }}
8490
run: |
85-
if [[ "$BUMP" != "patch" ]]; then
91+
# 'auto' is allowed here — the "Resolve auto bump" step maps it to 'patch'
92+
# on a hotfix line (hotfixes are patch-only by definition).
93+
if [[ "$BUMP" != "patch" && "$BUMP" != "auto" ]]; then
8694
echo "::error::Hotfix branches only support 'patch' bumps. Got: '$BUMP'"
8795
exit 1
8896
fi
@@ -158,11 +166,78 @@ jobs:
158166
echo "✅ $AHEAD new commit(s) on '$BRANCH' since $LATEST_TAG"
159167
fi
160168
169+
- name: Resolve auto bump
170+
id: resolve-bump
171+
if: github.event.inputs.version_bump == 'auto'
172+
env:
173+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
174+
MODE: ${{ steps.branch-mode.outputs.mode }}
175+
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
176+
run: |
177+
# 'auto' only chooses minor-vs-patch on main; hotfix lines are patch-only.
178+
if [[ "$MODE" != "main" ]]; then
179+
echo "resolved=patch" >> "$GITHUB_OUTPUT"
180+
echo "::notice::auto bump on hotfix line -> patch"
181+
exit 0
182+
fi
183+
184+
# Classify PRs merged into main after the base release was published:
185+
# any [FEAT]/[GATED-FEAT] title (the only feature PR types in the
186+
# contribution guide) => minor, otherwise patch. Every failure below
187+
# degrades to patch — never over-bump the public version, and never
188+
# hard-fail the release on a transient query hiccup. '// empty' keeps a
189+
# JSON null from leaking through as the literal "null" (matches the
190+
# get-latest step's convention).
191+
SINCE=$(gh api "repos/${{ github.repository }}/releases/tags/$LATEST_TAG" --jq '.published_at // empty' 2>/tmp/gh_since.err || echo "")
192+
if [[ -z "$SINCE" ]]; then
193+
echo "::warning::Could not resolve publish time for $LATEST_TAG; defaulting to patch."
194+
cat /tmp/gh_since.err >&2 || true
195+
echo "resolved=patch" >> "$GITHUB_OUTPUT"
196+
exit 0
197+
fi
198+
199+
LIMIT=200
200+
if ! PR_JSON=$(gh pr list --repo "${{ github.repository }}" \
201+
--base main --state merged --limit "$LIMIT" --search "merged:>$SINCE" \
202+
--json title 2>/tmp/gh_pr.err); then
203+
echo "::warning::PR classification query failed; defaulting to patch."
204+
cat /tmp/gh_pr.err >&2 || true # surface 403/permission vs rate-limit/5xx
205+
RESOLVED=patch
206+
else
207+
# Parse defensively: malformed/non-array stdout must degrade to patch,
208+
# not crash the job under `set -e`/pipefail.
209+
TOTAL=$(jq 'length' <<<"$PR_JSON" 2>/dev/null) || TOTAL=""
210+
FEAT_COUNT=$(jq '[.[] | select(.title | test("\\[(FEAT|GATED-FEAT)\\]"; "i"))] | length' <<<"$PR_JSON" 2>/dev/null) || FEAT_COUNT=""
211+
if ! [[ "$TOTAL" =~ ^[0-9]+$ && "$FEAT_COUNT" =~ ^[0-9]+$ ]]; then
212+
echo "::warning::PR classification returned unparseable output; defaulting to patch."
213+
RESOLVED=patch
214+
elif [[ "$FEAT_COUNT" -gt 0 ]]; then
215+
RESOLVED=minor
216+
else
217+
RESOLVED=patch
218+
# Truncation only changes the outcome when no FEAT was found within
219+
# the cap, so only warn in that case (a FEAT beyond it could be missed).
220+
if [[ "$TOTAL" -ge "$LIMIT" ]]; then
221+
echo "::warning::merged-PR query hit the $LIMIT-result cap since $SINCE; a FEAT PR beyond it may be missed — double-check the bump (override with version_bump=minor if needed)."
222+
fi
223+
fi
224+
fi
225+
226+
echo "resolved=$RESOLVED" >> "$GITHUB_OUTPUT"
227+
if [[ "$RESOLVED" == "minor" ]]; then
228+
echo "::notice::auto bump: $FEAT_COUNT FEAT/GATED-FEAT PR(s) merged since $LATEST_TAG -> minor"
229+
else
230+
echo "::notice::auto bump: no FEAT/GATED-FEAT PR merged since $LATEST_TAG -> patch. Re-run with version_bump=minor to override."
231+
fi
232+
161233
- name: Compute next version
162234
id: compute-version
163235
env:
164236
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
165-
BUMP_TYPE: ${{ github.event.inputs.version_bump }}
237+
# Non-auto: use the raw input. Auto: use the resolved value, falling back
238+
# to 'patch' (never back to 'auto', which would hit compute-version's
239+
# unknown-bump error).
240+
BUMP_TYPE: ${{ github.event.inputs.version_bump != 'auto' && github.event.inputs.version_bump || steps.resolve-bump.outputs.resolved || 'patch' }}
166241
run: |
167242
VERSION="${LATEST_TAG#v}"
168243
MAJOR=$(echo "$VERSION" | cut -d. -f1)
@@ -225,17 +300,20 @@ jobs:
225300
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
226301
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
227302
BUMP: ${{ github.event.inputs.version_bump }}
303+
RESOLVED: ${{ steps.resolve-bump.outputs.resolved }}
228304
PRERELEASE: ${{ github.event.inputs.pre_release }}
229305
MODE: ${{ steps.branch-mode.outputs.mode }}
230306
run: |
307+
BUMP_DISPLAY="$BUMP"
308+
[[ "$BUMP" == "auto" ]] && BUMP_DISPLAY="auto -> ${RESOLVED}"
231309
{
232310
echo "## Dry Run Summary"
233311
echo ""
234312
echo "| Field | Value |"
235313
echo "|-------|-------|"
236314
echo "| Mode | $MODE |"
237315
echo "| Current version | $LATEST_TAG |"
238-
echo "| Bump type | $BUMP |"
316+
echo "| Bump type | $BUMP_DISPLAY |"
239317
echo "| Next version | $NEW_TAG |"
240318
echo "| Pre-release | $PRERELEASE |"
241319
echo ""
@@ -324,9 +402,12 @@ jobs:
324402
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
325403
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
326404
BUMP: ${{ github.event.inputs.version_bump }}
405+
RESOLVED: ${{ steps.resolve-bump.outputs.resolved }}
327406
PRERELEASE: ${{ github.event.inputs.pre_release }}
328407
MODE: ${{ steps.branch-mode.outputs.mode }}
329408
run: |
409+
BUMP_DISPLAY="$BUMP"
410+
[[ "$BUMP" == "auto" ]] && BUMP_DISPLAY="auto -> ${RESOLVED}"
330411
{
331412
echo "## Release Created"
332413
echo ""
@@ -335,7 +416,7 @@ jobs:
335416
echo "| Mode | $MODE |"
336417
echo "| Previous version | $LATEST_TAG |"
337418
echo "| New version | $NEW_TAG |"
338-
echo "| Bump type | $BUMP |"
419+
echo "| Bump type | $BUMP_DISPLAY |"
339420
echo "| Pre-release | $PRERELEASE |"
340421
echo "| Triggered by | ${{ github.actor }} |"
341422
echo ""

0 commit comments

Comments
 (0)