Skip to content

Commit 976b9ba

Browse files
authored
chore(py): add a rc workflow for bug bashes (#4943)
1 parent a55b63c commit 976b9ba

3 files changed

Lines changed: 238 additions & 52 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
# Release Python RC: Run workflow → enter target version (e.g. 0.5.2) → publishes 0.5.2-rc.1, 0.5.2-rc.2, etc.
18+
19+
name: Release Python RC
20+
21+
on:
22+
workflow_dispatch:
23+
inputs:
24+
target_version:
25+
description: Target version (e.g., 0.5.2)
26+
type: string
27+
required: true
28+
29+
jobs:
30+
release_rc:
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: write
34+
steps:
35+
- uses: actions/checkout@v5
36+
with:
37+
token: ${{ secrets.GENKIT_RELEASER_GITHUB_TOKEN }}
38+
fetch-depth: 0
39+
40+
- name: Pull latest & fetch tags
41+
run: git pull origin main && git fetch --tags
42+
43+
- name: Validate target version
44+
run: |
45+
TARGET="${{ inputs.target_version }}"
46+
if ! [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
47+
echo "Error: target_version must be X.Y.Z (e.g., 0.6.0)"
48+
exit 1
49+
fi
50+
51+
- uses: astral-sh/setup-uv@v5
52+
with:
53+
enable-cache: true
54+
python-version: "3.12"
55+
version: "0.6.6"
56+
57+
- name: Create release branch & bump version
58+
id: rc
59+
run: |
60+
git config user.email "genkit-releaser@google.com" && git config user.name "genkit-releaser"
61+
TARGET="${{ inputs.target_version }}"
62+
BRANCH="release/py/${TARGET}"
63+
if git show-ref --quiet "refs/remotes/origin/${BRANCH}"; then
64+
git fetch origin "${BRANCH}" && git checkout -B "${BRANCH}" "origin/${BRANCH}"
65+
else
66+
git checkout -b "${BRANCH}"
67+
fi
68+
EXISTING=$(git tag -l "py/v${TARGET}-rc.*" 2>/dev/null || true)
69+
if [ -z "$EXISTING" ]; then RC_NUM=1; else
70+
RC_NUM=$(($(echo "$EXISTING" | sed -n 's/.*-rc\.\([0-9]*\)$/\1/p' | sort -n | tail -1) + 1))
71+
fi
72+
CUR=$(grep '^version = ' py/packages/genkit/pyproject.toml | cut -d'"' -f2)
73+
NEW="${TARGET}-rc.${RC_NUM}"
74+
echo "new=${NEW}" >> $GITHUB_OUTPUT
75+
cd py
76+
for f in packages/genkit/pyproject.toml plugins/*/pyproject.toml; do
77+
[ -f "$f" ] && grep -q "version = \"$CUR\"" "$f" && sed -i "s/version = \"$CUR\"/version = \"$NEW\"/" "$f"
78+
done
79+
uv lock && cd ..
80+
git add py/ && git commit -m "chore(py): bump version to $NEW" && git push origin "${BRANCH}"
81+
82+
- name: Create tag & GitHub release
83+
env:
84+
GH_TOKEN: ${{ secrets.GENKIT_RELEASER_GITHUB_TOKEN }}
85+
run: gh release create "py/v${{ steps.rc.outputs.new }}" --title "Genkit Python SDK v${{ steps.rc.outputs.new }}" --notes "Release candidate." --prerelease

py/bin/create_release

Lines changed: 141 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@
1919
#
2020
# Usage:
2121
# ./bin/create_release <VERSION> [PR_NUMBER]
22+
# ./bin/create_release <VERSION> --notes "Release notes text"
23+
# ./bin/create_release <VERSION> --notes-file path/to/notes.md
2224
#
2325
# Examples:
24-
# ./bin/create_release 0.5.0 4417 # Use PR #4417's description
25-
# ./bin/create_release 0.5.0 # Auto-find merged PR for this version
26+
# ./bin/create_release 0.5.0 4417 # Use PR #4417's description
27+
# ./bin/create_release 0.5.0 # Auto-find merged PR for this version
28+
# ./bin/create_release 0.5.0rc1 --notes "RC1 for 0.5.0" # Release candidate, no PR
29+
# ./bin/create_release 0.5.0rc1 --notes-file rc-notes.md
2630
#
2731
# This script will:
2832
# 1. Verify you're on main branch with latest changes
29-
# 2. Fetch the release PR description from GitHub
33+
# 2. Fetch release notes (from PR or --notes/--notes-file)
3034
# 3. Create an annotated git tag (py/v<VERSION>)
3135
# 4. Push the tag to origin
32-
# 5. Create a GitHub release using the PR description
36+
# 5. Create a GitHub release
3337

3438
set -euo pipefail
3539

@@ -40,17 +44,78 @@ YELLOW='\033[1;33m'
4044
BLUE='\033[0;34m'
4145
NC='\033[0m' # No Color
4246

43-
# Check arguments
44-
if [ $# -lt 1 ]; then
47+
# Parse arguments
48+
VERSION=""
49+
PR_NUMBER=""
50+
NOTES_STRING=""
51+
NOTES_FILE=""
52+
53+
while [[ $# -gt 0 ]]; do
54+
case "$1" in
55+
--notes)
56+
if [[ -z "${2:-}" || "$2" == -* ]]; then
57+
echo -e "${RED}Error: --notes requires a value.${NC}"
58+
exit 1
59+
fi
60+
NOTES_STRING="$2"
61+
shift 2
62+
;;
63+
--notes-file)
64+
if [[ -z "${2:-}" || "$2" == -* ]]; then
65+
echo -e "${RED}Error: --notes-file requires a value.${NC}"
66+
exit 1
67+
fi
68+
NOTES_FILE="$2"
69+
shift 2
70+
;;
71+
-h|--help)
72+
echo "Usage: $0 <VERSION> [PR_NUMBER]"
73+
echo " $0 <VERSION> --notes \"...\""
74+
echo " $0 <VERSION> --notes-file path/to/notes.md"
75+
echo ""
76+
echo "Options:"
77+
echo " --notes TEXT Use this text as release notes (skips PR lookup)"
78+
echo " --notes-file F Use file contents as release notes (skips PR lookup)"
79+
exit 0
80+
;;
81+
*)
82+
if [ -z "$VERSION" ]; then
83+
VERSION="$1"
84+
elif [ -z "$PR_NUMBER" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
85+
PR_NUMBER="$1"
86+
else
87+
echo -e "${RED}Error: Unexpected argument: $1${NC}"
88+
exit 1
89+
fi
90+
shift
91+
;;
92+
esac
93+
done
94+
95+
if [ -z "$VERSION" ]; then
4596
echo -e "${RED}Usage: $0 <VERSION> [PR_NUMBER]${NC}"
46-
echo "Examples:"
47-
echo " $0 0.5.0 4417 # Use specific PR"
48-
echo " $0 0.5.0 # Auto-find merged PR"
97+
echo " $0 <VERSION> --notes \"...\""
98+
echo " $0 <VERSION> --notes-file path/to/notes.md"
99+
exit 1
100+
fi
101+
102+
# Can't use both notes options
103+
if [ -n "$NOTES_STRING" ] && [ -n "$NOTES_FILE" ]; then
104+
echo -e "${RED}Error: Use only one of --notes or --notes-file${NC}"
49105
exit 1
50106
fi
51107

52-
VERSION="$1"
53-
PR_NUMBER="${2:-}"
108+
# If using notes-file, verify file exists
109+
if [ -n "$NOTES_FILE" ] && [ ! -f "$NOTES_FILE" ]; then
110+
echo -e "${RED}Error: Notes file not found: ${NOTES_FILE}${NC}"
111+
exit 1
112+
fi
113+
114+
USE_PR_NOTES=true
115+
if [ -n "$NOTES_STRING" ] || [ -n "$NOTES_FILE" ]; then
116+
USE_PR_NOTES=false
117+
fi
118+
54119
TAG_NAME="py/v${VERSION}"
55120

56121
echo -e "${BLUE}=== Genkit Python SDK Release: v${VERSION} ===${NC}"
@@ -110,61 +175,74 @@ echo -e "${YELLOW}Step 3: Pulling latest changes...${NC}"
110175
git pull origin main
111176
echo -e "${GREEN}✓ Pulled latest from origin/main${NC}"
112177

113-
# Step 4: Find or verify PR
114-
echo ""
115-
echo -e "${YELLOW}Step 4: Finding release PR...${NC}"
116-
if [ -z "$PR_NUMBER" ]; then
117-
# Auto-find the merged PR for this version
118-
PR_NUMBER=$(gh pr list --repo firebase/genkit --state merged \
119-
--search "Python SDK ${VERSION} in:title" \
120-
--json number --limit 1 | jq -r '.[0].number // empty')
121-
178+
# Step 4 & 5: Get release notes (from PR or --notes/--notes-file)
179+
RELEASE_NOTES_FILE=$(mktemp)
180+
trap 'rm -f "$RELEASE_NOTES_FILE"' EXIT
181+
182+
if [ "$USE_PR_NOTES" = true ]; then
183+
echo ""
184+
echo -e "${YELLOW}Step 4: Finding release PR...${NC}"
122185
if [ -z "$PR_NUMBER" ]; then
123-
# Try searching by version in body
186+
# Auto-find the merged PR for this version
124187
PR_NUMBER=$(gh pr list --repo firebase/genkit --state merged \
125-
--search "v${VERSION} label:python" \
188+
--search "Python SDK ${VERSION} in:title" \
126189
--json number --limit 1 | jq -r '.[0].number // empty')
190+
191+
if [ -z "$PR_NUMBER" ]; then
192+
# Try searching by version in body
193+
PR_NUMBER=$(gh pr list --repo firebase/genkit --state merged \
194+
--search "v${VERSION} label:python" \
195+
--json number --limit 1 | jq -r '.[0].number // empty')
196+
fi
197+
198+
if [ -z "$PR_NUMBER" ]; then
199+
echo -e "${RED}Error: Could not find merged PR for version ${VERSION}${NC}"
200+
echo "Specify PR number manually: $0 ${VERSION} <PR_NUMBER>"
201+
echo "Or use --notes or --notes-file for release candidates"
202+
exit 1
203+
fi
127204
fi
128-
129-
if [ -z "$PR_NUMBER" ]; then
130-
echo -e "${RED}Error: Could not find merged PR for version ${VERSION}${NC}"
131-
echo "Specify PR number manually: $0 ${VERSION} <PR_NUMBER>"
132-
exit 1
133-
fi
134-
fi
135205

136-
echo -e "${GREEN}✓ Found PR #${PR_NUMBER}${NC}"
206+
echo -e "${GREEN}✓ Found PR #${PR_NUMBER}${NC}"
137207

138-
# Fetch PR description
139-
echo ""
140-
echo -e "${YELLOW}Step 5: Fetching PR description...${NC}"
141-
RELEASE_NOTES_FILE=$(mktemp)
142-
gh pr view "$PR_NUMBER" --repo firebase/genkit --json body --jq '.body' > "$RELEASE_NOTES_FILE"
208+
echo ""
209+
echo -e "${YELLOW}Step 5: Fetching PR description...${NC}"
210+
gh pr view "$PR_NUMBER" --repo firebase/genkit --json body --jq '.body' > "$RELEASE_NOTES_FILE"
143211

144-
if [ ! -s "$RELEASE_NOTES_FILE" ]; then
145-
echo -e "${RED}Error: PR #${PR_NUMBER} has no description${NC}"
146-
rm -f "$RELEASE_NOTES_FILE"
147-
exit 1
148-
fi
212+
if [ ! -s "$RELEASE_NOTES_FILE" ]; then
213+
echo -e "${RED}Error: PR #${PR_NUMBER} has no description${NC}"
214+
exit 1
215+
fi
149216

150-
LINE_COUNT=$(wc -l < "$RELEASE_NOTES_FILE")
151-
echo -e "${GREEN}✓ Fetched PR description (${LINE_COUNT} lines)${NC}"
217+
LINE_COUNT=$(wc -l < "$RELEASE_NOTES_FILE")
218+
echo -e "${GREEN}✓ Fetched PR description (${LINE_COUNT} lines)${NC}"
219+
else
220+
echo ""
221+
echo -e "${YELLOW}Step 4: Using provided release notes...${NC}"
222+
if [ -n "$NOTES_FILE" ]; then
223+
cp "$NOTES_FILE" "$RELEASE_NOTES_FILE"
224+
echo -e "${GREEN}✓ Using notes from ${NOTES_FILE}${NC}"
225+
else
226+
echo "$NOTES_STRING" > "$RELEASE_NOTES_FILE"
227+
echo -e "${GREEN}✓ Using inline notes${NC}"
228+
fi
229+
fi
152230

153231
# Step 6: Check if tag already exists
154232
echo ""
155233
echo -e "${YELLOW}Step 6: Checking if tag exists...${NC}"
156234
if git tag -l "$TAG_NAME" | grep -q "$TAG_NAME"; then
157235
echo -e "${RED}Error: Tag ${TAG_NAME} already exists${NC}"
158236
echo "Delete it first if you need to recreate: git tag -d ${TAG_NAME} && git push origin :refs/tags/${TAG_NAME}"
159-
rm -f "$RELEASE_NOTES_FILE"
160237
exit 1
161238
fi
162239
echo -e "${GREEN}✓ Tag ${TAG_NAME} does not exist${NC}"
163240

164241
# Step 7: Create tag
165242
echo ""
166243
echo -e "${YELLOW}Step 7: Creating annotated tag...${NC}"
167-
git tag -a "$TAG_NAME" -m "Genkit Python SDK v${VERSION}
244+
if [ "$USE_PR_NOTES" = true ]; then
245+
TAG_MSG="Genkit Python SDK v${VERSION}
168246
169247
Release highlights:
170248
- See py/CHANGELOG.md for full release notes
@@ -173,6 +251,17 @@ Release highlights:
173251
Published packages:
174252
- genkit (core)
175253
- genkit-plugin-* (22 plugins)"
254+
else
255+
TAG_MSG="Genkit Python SDK v${VERSION}
256+
257+
Release highlights:
258+
- See py/CHANGELOG.md for full release notes
259+
260+
Published packages:
261+
- genkit (core)
262+
- genkit-plugin-* (22 plugins)"
263+
fi
264+
git tag -a "$TAG_NAME" -m "$TAG_MSG"
176265

177266
echo -e "${GREEN}✓ Created tag: ${TAG_NAME}${NC}"
178267

@@ -185,21 +274,22 @@ echo -e "${GREEN}✓ Pushed tag to origin${NC}"
185274
# Step 9: Create GitHub release
186275
echo ""
187276
echo -e "${YELLOW}Step 9: Creating GitHub release...${NC}"
188-
gh release create "$TAG_NAME" \
189-
--title "Genkit Python SDK v${VERSION}" \
190-
--notes-file "$RELEASE_NOTES_FILE"
277+
GH_RELEASE_ARGS=(--title "Genkit Python SDK v${VERSION}" --notes-file "$RELEASE_NOTES_FILE")
278+
if [[ "$VERSION" =~ rc[0-9]*$ ]] || [[ "$VERSION" =~ -rc\.[0-9]+$ ]]; then
279+
GH_RELEASE_ARGS+=(--prerelease)
280+
fi
281+
gh release create "$TAG_NAME" "${GH_RELEASE_ARGS[@]}"
191282

192283
echo -e "${GREEN}✓ Created GitHub release${NC}"
193284

194-
# Cleanup
195-
rm -f "$RELEASE_NOTES_FILE"
196-
197285
# Summary
198286
echo ""
199287
echo -e "${BLUE}=== Release Created Successfully ===${NC}"
200288
echo ""
201289
echo "Tag: ${TAG_NAME}"
202-
echo "Based on PR: #${PR_NUMBER}"
290+
if [ "$USE_PR_NOTES" = true ]; then
291+
echo "Based on PR: #${PR_NUMBER}"
292+
fi
203293
echo "Release: https://github.com/firebase/genkit/releases/tag/${TAG_NAME}"
204294
echo ""
205295
echo -e "${YELLOW}Next steps:${NC}"

py/docs/release_playbook.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Python Release Playbook
22

3-
## Release steps
3+
## Release candidates (RC)
4+
5+
Use the **Release Python RC** workflow in GitHub Actions:
6+
7+
1. Go to Actions → Release Python RC
8+
2. Click "Run workflow"
9+
3. Enter target version (e.g., `0.5.2`)
10+
4. The workflow infers the next RC (`0.5.2-rc.1`, `0.5.2-rc.2`, …) and publishes it
11+
12+
No PR required. The workflow bumps version, commits to the release branch, creates the tag and GitHub release, and triggers publish to PyPI.
13+
14+
## Stable release steps
415

516
1. `./bin/bump_version 0.7.1` — bump all `pyproject.toml` files
617
2. `./bin/release_check` — preflight checks

0 commit comments

Comments
 (0)