- 
        Couldn't load subscription status. 
- Fork 548
chore: agentic workflow for automatic version bump #1947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| Summary of ChangesHello @yzh119, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces an agentic workflow designed to automate the process of semantic version bumping for the project. By integrating a new Python script that utilizes Google Gemini AI, the system can intelligently analyze recent commit history to decide whether a major, minor, or patch version increment is required. This automation aims to streamline release management, reduce manual effort, and ensure consistent application of semantic versioning principles. Highlights
 Ignored Files
 Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either  
 Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a  Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
 | 
| Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds two GitHub Actions workflows and two Python CLIs: a bump-version workflow that uses AI (or manual override) to determine semantic version bumps and create a PR, and an auto-tag-and-release workflow that tags and triggers a release when a labeled version-bump PR is merged. Also adds PR body generator and AI analysis scripts. Changes
 Sequence Diagram(s)sequenceDiagram
    participant Scheduler as User / Scheduler
    participant GHA as GitHub Actions
    participant Repo as Repository
    participant Git as Git
    participant CLI_AI as scripts/ai_determine_version_bump.py
    participant PRBody as scripts/generate_bump_pr_body.py
    participant AI as AI Providers (OpenAI / Claude / Gemini)
    participant Fallback as Heuristic
    participant GHCLI as GitHub CLI / peter-evans
    Scheduler->>GHA: trigger bump-version (manual / scheduled)
    GHA->>Repo: checkout & read `version.txt`
    GHA->>GHCLI: check for open version-bump PR
    alt existing PR found
        GHA-->>Scheduler: note & skip creating duplicate PR
    else no PR
        GHA->>Git: get latest tag & commits
        GHA->>CLI_AI: analyze commits + current_version
        CLI_AI->>AI: try OpenAI → Claude → Gemini
        alt AI returns result
            AI-->>CLI_AI: bump_type, reasoning, key_changes
        else all AI fail
            CLI_AI->>Fallback: keyword heuristic
            Fallback-->>CLI_AI: bump_type, key_changes
        end
        CLI_AI-->>GHA: normalized result
        GHA->>PRBody: generate PR body (current/new/bump/reasoning)
        alt bump needed
            GHA->>Repo: update `version.txt`
            GHA->>GHCLI: create PR (peter-evans/create-pull-request)
            GHA-->>Scheduler: PR created summary
        else no bump
            GHA-->>Scheduler: no bump required
        end
    end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
 Pre-merge checks and finishing touches❌ Failed checks (1 warning)
 ✅ Passed checks (2 passed)
 ✨ Finishing touches
 🧪 Generate unit tests (beta)
 Comment  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new script to automatically determine semantic version bumps by analyzing commit messages with the Gemini AI. The script is well-structured, with a fallback mechanism for when the AI is unavailable.
My review focuses on improving the robustness and correctness of the script. I've identified a critical bug in how git commit messages are parsed, which could lead to incorrect analysis, and another issue with an invalid AI model name that would cause the script to fail. The suggested changes will fix these issues and also improve error handling.
        
          
                scripts/ai_determine_version_bump.py
              
                Outdated
          
        
      | def get_commits_since_tag(tag: str, max_commits: int = 100) -> list[dict]: | ||
| """Get commit messages and diffs since the given tag.""" | ||
| try: | ||
| if tag: | ||
| cmd = [ | ||
| "git", | ||
| "log", | ||
| f"{tag}..HEAD", | ||
| "--pretty=format:%H|||%s|||%b", | ||
| f"-{max_commits}", | ||
| ] | ||
| else: | ||
| cmd = ["git", "log", "--pretty=format:%H|||%s|||%b", f"-{max_commits}"] | ||
|  | ||
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) | ||
|  | ||
| commits = [] | ||
| for line in result.stdout.strip().split("\n"): | ||
| if not line.strip(): | ||
| continue | ||
| parts = line.split("|||") | ||
| if len(parts) >= 2: | ||
| commit_hash = parts[0] | ||
| subject = parts[1] | ||
| body = parts[2] if len(parts) > 2 else "" | ||
|  | ||
| # Get file changes for this commit | ||
| diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"] | ||
| diff_result = subprocess.run(diff_cmd, capture_output=True, text=True) | ||
| files_changed = diff_result.stdout.strip() | ||
|  | ||
| commits.append( | ||
| { | ||
| "hash": commit_hash[:7], | ||
| "subject": subject, | ||
| "body": body, | ||
| "files_changed": files_changed, | ||
| } | ||
| ) | ||
|  | ||
| return commits | ||
| except subprocess.CalledProcessError: | ||
| return [] | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current method of parsing git log output is not robust. It splits the output by newlines, which will fail if a commit body contains newlines, leading to incorrect parsing. A better approach is to use a null byte (%x00) as a record separator for commits.
Additionally:
- The subprocess.runcall to get the diff does not havecheck=True, which means errors will be silently ignored.
- The current implementation makes a separate subprocesscall for each commit to get file stats, which is inefficient (N+1 problem). While the following suggestion maintains that for simplicity, be aware that it could be slow for a large number of commits.
The suggested change fixes the parsing and error handling issues.
def get_commits_since_tag(tag: str, max_commits: int = 100) -> list[dict]:
    """Get commit messages and diffs since the given tag."""
    try:
        log_format = "%H|||%s|||%b"
        if tag:
            cmd = [
                "git",
                "log",
                f"{tag}..HEAD",
                f"--pretty=format:{log_format}%x00",
                f"-{max_commits}",
            ]
        else:
            cmd = ["git", "log", f"--pretty=format:{log_format}%x00", f"-{max_commits}"]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        commits = []
        for commit_data in filter(None, result.stdout.split("\x00")):
            parts = commit_data.split("|||", 2)
            if len(parts) < 2:
                continue
            commit_hash = parts[0]
            subject = parts[1]
            body = parts[2] if len(parts) > 2 else ""
            # Get file changes for this commit
            diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"]
            diff_result = subprocess.run(
                diff_cmd, capture_output=True, text=True, check=True
            )
            files_changed = diff_result.stdout.strip()
            commits.append(
                {
                    "hash": commit_hash[:7],
                    "subject": subject,
                    "body": body,
                    "files_changed": files_changed,
                }
            )
        return commits
    except subprocess.CalledProcessError:
        return []There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (5)
scripts/ai_determine_version_bump.py (3)
69-71: Handlegit differrors explicitly.Use
check=Trueor guard the output; current code ignores failures silently.You can keep
check=Falsebut defaultfiles_changedto""(as in the diff in my earlier comment). Ensure this is intentional.
249-258: Minor: unused suffix and exception messages.
suffixfromparse_versionis unused inbump_version; also long f-strings inraisetrigger style rule. Prefix to_suffixand shorten messages.- major, minor, patch, suffix = match.groups() - return int(major), int(minor), int(patch), suffix + major, minor, patch, suffix = match.groups() + return int(major), int(minor), int(patch), suffix @@ - major, minor, patch, suffix = parse_version(current_version) + major, minor, patch, _suffix = parse_version(current_version) @@ - else: - raise ValueError(f"Invalid bump type: {bump_type}") + else: + raise ValueError("Invalid bump type")Also applies to: 262-271
1-12: Shebang vs executable bit.Either remove the shebang or mark the file executable in git (chmod +x) to satisfy EXE001.
-#!/usr/bin/env python3 +#!/usr/bin/env python3Action: set executable bit, or drop the shebang if always invoked via
python .....github/workflows/bump-version.yml (2)
170-217: Avoidsedsubstitution for multiline reasoning; build the body directly.
sedfails with multi-line content and special chars. Compose via here-docs and simple appends; also use repo-aware absolute links.Apply:
- # Generate PR body - cat > /tmp/pr_body.md << 'EOF' -## Version Bump: v$CURRENT_VERSION → v$NEW_VERSION - -This PR bumps the version from **v$CURRENT_VERSION** to **v$NEW_VERSION** ($BUMP_TYPE_UPPER bump). - -### AI Analysis - -$REASONING - -EOF + # Generate PR body (header) + BUMP_TYPE_UPPER=$(echo "$BUMP_TYPE" | tr '[:lower:]' '[:upper:]') + { + echo "## Version Bump: v$CURRENT_VERSION → v$NEW_VERSION" + echo + echo "This PR bumps the version from **v$CURRENT_VERSION** to **v$NEW_VERSION** ($BUMP_TYPE_UPPER bump)." + echo + echo "### AI Analysis" + echo + cat /tmp/reasoning.txt + } > /tmp/pr_body.md @@ - if [ -n "$KEY_CHANGES" ]; then - echo "" >> /tmp/pr_body.md - echo "### Key Changes" >> /tmp/pr_body.md - echo "" >> /tmp/pr_body.md - while IFS= read -r change; do - echo "- $change" >> /tmp/pr_body.md - done < /tmp/key_changes.txt - fi + if [ -s /tmp/key_changes.txt ]; then + { + echo + echo "### Key Changes" + echo + sed 's/^/- /' /tmp/key_changes.txt + } >> /tmp/pr_body.md + fi @@ -According to our [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md): +According to our [CONTRIBUTING.md]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/blob/main/CONTRIBUTING.md): @@ -After merging this PR: -1. Create a git tag: `git tag v$NEW_VERSION` -2. Run the [release workflow](../actions/workflows/release.yml) with tag `v$NEW_VERSION` +After merging this PR: +1. Create a git tag: `git tag v$NEW_VERSION` +2. Run the [release workflow]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/release.yml) with tag `v$NEW_VERSION` @@ -🤖 Auto-generated by [bump-version workflow](../actions/runs/$RUN_ID) +🤖 Auto-generated by [bump-version workflow]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/${{ github.run_id }}) EOF - - # Replace variables - sed -i "s/\$CURRENT_VERSION/$CURRENT_VERSION/g" /tmp/pr_body.md - sed -i "s/\$NEW_VERSION/$NEW_VERSION/g" /tmp/pr_body.md - sed -i "s/\$BUMP_TYPE_UPPER/$(echo $BUMP_TYPE | tr '[:lower:]' '[:upper:]')/g" /tmp/pr_body.md - sed -i "s|\$REASONING|$REASONING|g" /tmp/pr_body.md - sed -i "s/\$RUN_ID/${{ github.run_id }}/g" /tmp/pr_body.md
32-41: Pin SDK version for reproducibility.Unpinned
google-generativeaican introduce breaking SDK changes. Pin with a known-working version or a compatible range.- pip install google-generativeai + pip install 'google-generativeai==0.7.*'Ensure this matches the SDK API used in the script.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- .github/workflows/bump-version.yml(1 hunks)
- scripts/ai_determine_version_bump.py(1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.8)
.github/workflows/bump-version.yml
173-173: could not parse as YAML: could not find expected ':'
(syntax-check)
🪛 Ruff (0.14.0)
scripts/ai_determine_version_bump.py
1-1: Shebang is present but file is not executable
(EXE001)
27-27: Starting a process with a partial executable path
(S607)
37-37: Consider moving this statement to an else block
(TRY300)
56-56: subprocess call: check for execution of untrusted input
(S603)
70-70: subprocess call: check for execution of untrusted input
(S603)
82-82: Consider moving this statement to an else block
(TRY300)
173-173: Consider moving this statement to an else block
(TRY300)
181-181: Do not catch blind exception: Exception
(BLE001)
254-254: Avoid specifying long messages outside the exception class
(TRY003)
262-262: Unpacked variable suffix is never used
Prefix it with an underscore or any other dummy variable pattern
(RUF059)
271-271: Avoid specifying long messages outside the exception class
(TRY003)
🪛 YAMLlint (1.37.1)
.github/workflows/bump-version.yml
[error] 175-175: syntax error: could not find expected ':'
(syntax)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy Docs
🔇 Additional comments (1)
scripts/ai_determine_version_bump.py (1)
154-173: Original code is correct; review comment's concerns about SDK surface instability are unfounded.The current google-generativeai SDK uses
response.textas the standard accessor for extracting text from responses, andgenai.types.GenerationConfigis the proper mechanism for passing generation configuration. Your existing code already follows these best practices. The suggested fallback to internal implementation details likeresponse.candidates[0].content.parts[0].textis unnecessary and contradicts the SDK's documented public API contract.The concern about version variance appears unfounded; the
.textaccessor is the stable, uniform interface across SDK versions.Likely an incorrect or invalid review comment.
| EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "") | ||
| if [ -n "$EXISTING_PR" ]; then | ||
| echo "has_existing_pr=true" >> $GITHUB_OUTPUT | ||
| echo "existing_pr_number=${EXISTING_PR}" >> $GITHUB_OUTPUT | ||
| echo "⚠️ Existing version bump PR found: #${EXISTING_PR}" | ||
| else | ||
| echo "has_existing_pr=false" >> $GITHUB_OUTPUT | ||
| echo "✓ No existing version bump PR found" | ||
| fi | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against null from gh pr list to avoid false positives.
Current test treats "null" as a PR number. Use // empty.
Apply:
-          EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "")
+          EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number // empty' || echo "")
           if [ -n "$EXISTING_PR" ]; then📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "") | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "has_existing_pr=true" >> $GITHUB_OUTPUT | |
| echo "existing_pr_number=${EXISTING_PR}" >> $GITHUB_OUTPUT | |
| echo "⚠️ Existing version bump PR found: #${EXISTING_PR}" | |
| else | |
| echo "has_existing_pr=false" >> $GITHUB_OUTPUT | |
| echo "✓ No existing version bump PR found" | |
| fi | |
| EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number // empty' || echo "") | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "has_existing_pr=true" >> $GITHUB_OUTPUT | |
| echo "existing_pr_number=${EXISTING_PR}" >> $GITHUB_OUTPUT | |
| echo "⚠️ Existing version bump PR found: #${EXISTING_PR}" | |
| else | |
| echo "has_existing_pr=false" >> $GITHUB_OUTPUT | |
| echo "✓ No existing version bump PR found" | |
| fi | 
🤖 Prompt for AI Agents
.github/workflows/bump-version.yml around lines 55 to 63: the current GH CLI jq
expression can return the string "null" which gets treated as a non-empty PR
number; update the jq part to coerce null to an empty string (use .[0].number //
empty) so EXISTING_PR will be empty when no PR exists, leaving the rest of the
if-check unchanged.
        
          
                .github/workflows/bump-version.yml
              
                Outdated
          
        
      | # Run AI analysis | ||
| python scripts/ai_determine_version_bump.py \ | ||
| --output-format json \ | ||
| --verbose > /tmp/analysis.json 2>&1 | ||
|  | ||
| # Extract results | ||
| BUMP_TYPE=$(jq -r '.bump_type' /tmp/analysis.json) | ||
| NEW_VERSION=$(jq -r '.new_version' /tmp/analysis.json) | ||
| REASONING=$(jq -r '.reasoning' /tmp/analysis.json) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not merge stderr into the JSON file (breaks jq).
--verbose writes to stderr; > /tmp/analysis.json 2>&1 corrupts JSON. Capture stdout only.
Apply:
-          python scripts/ai_determine_version_bump.py \
-            --output-format json \
-            --verbose > /tmp/analysis.json 2>&1
+          python scripts/ai_determine_version_bump.py \
+            --output-format json \
+            --verbose 1> /tmp/analysis.json 2> /tmp/analysis.log
+          echo "AI analysis logs saved to /tmp/analysis.log"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Run AI analysis | |
| python scripts/ai_determine_version_bump.py \ | |
| --output-format json \ | |
| --verbose > /tmp/analysis.json 2>&1 | |
| # Extract results | |
| BUMP_TYPE=$(jq -r '.bump_type' /tmp/analysis.json) | |
| NEW_VERSION=$(jq -r '.new_version' /tmp/analysis.json) | |
| REASONING=$(jq -r '.reasoning' /tmp/analysis.json) | |
| # Run AI analysis | |
| python scripts/ai_determine_version_bump.py \ | |
| --output-format json \ | |
| --verbose 1> /tmp/analysis.json 2> /tmp/analysis.log | |
| echo "AI analysis logs saved to /tmp/analysis.log" | |
| # Extract results | |
| BUMP_TYPE=$(jq -r '.bump_type' /tmp/analysis.json) | |
| NEW_VERSION=$(jq -r '.new_version' /tmp/analysis.json) | |
| REASONING=$(jq -r '.reasoning' /tmp/analysis.json) | 
🤖 Prompt for AI Agents
In .github/workflows/bump-version.yml around lines 73–81, the workflow redirects
stderr into the JSON output ("> /tmp/analysis.json 2>&1") which corrupts the
JSON; change the invocation to capture stdout only (remove the "2>&1" redirect)
or redirect stderr to a separate file (e.g., "2>/tmp/analysis.err"), or drop the
--verbose flag so the script writes pure JSON to stdout; ensure jq reads
/tmp/analysis.json only and optionally fail the job if jq parsing fails.
        
          
                scripts/ai_determine_version_bump.py
              
                Outdated
          
        
      | "--max-commits", | ||
| type=int, | ||
| default=100, | ||
| help="Maximum number of commits to analyze", | ||
| ) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate --max-commits and use -n explicitly (avoid --5 on negatives).
Negative values produce malformed flags; also -n <N> is clearer.
Apply:
-    parser.add_argument(
+    parser.add_argument(
         "--max-commits",
         type=int,
         default=100,
         help="Maximum number of commits to analyze",
     )And fold into get_commits_since_tag change above which uses -n str(max(1, max_commits)). 
Also applies to: 46-55
🤖 Prompt for AI Agents
In scripts/ai_determine_version_bump.py around lines 287-291 (and also apply the
same change at lines ~46-55), validate the --max-commits argument to prevent
negative values and ensure the git invocation always uses the explicit '-n'
form; enforce this by converting/validating the parsed max_commits to an int >=
0 (or raise an argparse error) and when building the git command pass "-n",
str(max(1, max_commits)) so negatives never produce a malformed flag and the
one-source-of-truth logic in get_commits_since_tag is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (4)
.github/workflows/bump-version.yml (3)
194-202: YAML parse error: replace Unicode arrow with ASCII.The heading includes “→”, which actionlint/yamllint choke on. Use ASCII “->”.
Apply:
-## Version Bump: v$CURRENT_VERSION → v$NEW_VERSION +## Version Bump: v$CURRENT_VERSION -> v$NEW_VERSION
93-101: Don’t merge stderr into JSON; keep logs separate and fail if jq can’t parse.Current redirection corrupts /tmp/analysis.json.
Apply:
- python scripts/ai_determine_version_bump.py \ - --provider ${AI_PROVIDER} \ - --output-format json \ - --verbose > /tmp/analysis.json 2>&1 + set -euo pipefail + python scripts/ai_determine_version_bump.py \ + --provider "${AI_PROVIDER}" \ + --output-format json \ + --verbose 1> /tmp/analysis.json 2> /tmp/analysis.log + echo "AI analysis logs saved to /tmp/analysis.log" + # Validate JSON + jq -e . /tmp/analysis.json > /dev/null
64-71: Guard against null from gh/jq to avoid false positives.Coerce null to empty string.
Apply:
- EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "") + EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number // empty' || echo "")scripts/ai_determine_version_bump.py (1)
53-96: Parse commits robustly; current newline split drops bodies and BREAKING markers.Use record/field separators; add error handling.
Apply:
-def get_commits_since_tag(tag: str, max_commits: int = 100) -> list[dict]: +def get_commits_since_tag(tag: str, max_commits: int = 100) -> list[dict]: @@ - if tag: - cmd = [ - "git", - "log", - f"{tag}..HEAD", - "--pretty=format:%H|||%s|||%b", - f"-{max_commits}", - ] - else: - cmd = ["git", "log", "--pretty=format:%H|||%s|||%b", f"-{max_commits}"] - - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - - commits = [] - for line in result.stdout.strip().split("\n"): - if not line.strip(): - continue - parts = line.split("|||") - if len(parts) >= 2: - commit_hash = parts[0] - subject = parts[1] - body = parts[2] if len(parts) > 2 else "" - - # Get file changes for this commit - diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"] - diff_result = subprocess.run(diff_cmd, capture_output=True, text=True) - files_changed = diff_result.stdout.strip() + pretty = "%H%x1f%s%x1f%b%x1e" # field sep=\x1f, record sep=\x1e + n = str(max(1, int(max_commits))) + base = ["git", "log", "--pretty=format:" + pretty, "-n", n] + cmd = (["git", "log", f"{tag}..HEAD", "--pretty=format:" + pretty, "-n", n] if tag else base) + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + commits: list[dict] = [] + for rec in result.stdout.split("\x1e"): + if not rec.strip(): + continue + fields = rec.split("\x1f") + if len(fields) < 2: + continue + commit_hash, subject = fields[0], fields[1] + body = fields[2] if len(fields) > 2 else "" + + # Get file changes for this commit + diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"] + diff_result = subprocess.run(diff_cmd, capture_output=True, text=True, check=True) + files_changed = (diff_result.stdout or "").strip() @@ - return commits + return commits
🧹 Nitpick comments (6)
.github/workflows/bump-version.yml (2)
100-111: Robust key_changes extraction.Use jq defaults to avoid errors on missing/empty arrays.
Apply:
- jq -r '.key_changes[]' /tmp/analysis.json > /tmp/key_changes.txt || echo "" > /tmp/key_changes.txt + jq -r '.key_changes // [] | .[]' /tmp/analysis.json > /tmp/key_changes.txt || : > /tmp/key_changes.txt
250-266: Optional: token fallback and label creation.If FLASHINFER_BOT_TOKEN is unset/mis-scoped, prefer default GITHUB_TOKEN; ensure labels exist.
Example:
- token: ${{ secrets.FLASHINFER_BOT_TOKEN }} + token: ${{ secrets.FLASHINFER_BOT_TOKEN || github.token }}And consider a prep step to create labels idempotently with gh if missing.
scripts/ai_determine_version_bump.py (4)
230-271: Use a stable Gemini default.Defaulting to experimental “gemini-2.0-flash-exp” is risky; prefer a stable alias.
Apply:
- model_name = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp") + model_name = os.getenv("GEMINI_MODEL", "gemini-1.5-pro-latest")
384-386: Skip release/version commits incl. pre-releases.Current regex misses prerelease tags like rc, beta.
Apply:
- if re.search(r"bump version|release.*v?\d+\.\d+\.\d+", text): + if re.search(r"(bump version|release).*\bv?\d+\.\d+\.\d+(?:[0-9A-Za-z.-]+)?\b", text): continue
434-443: Minor: unused suffix variable from parse_version.Silence lint and clarify intent.
Apply:
- major, minor, patch, suffix = match.groups() + major, minor, patch, _suffix = match.groups() @@ - major, minor, patch, suffix = parse_version(current_version) + major, minor, patch, _ = parse_version(current_version)Also applies to: 447-456
471-476: Validate max-commits and use explicit -n form consistently.Prevents bad flags on negatives and keeps one source of truth.
- Ensure get_commits_since_tag uses max(1, max_commits) (included in my earlier diff).
- Optionally clamp in argparse with
type=lambda v: max(1, int(v)).Also applies to: 517-568
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- .github/workflows/bump-version.yml(1 hunks)
- scripts/ai_determine_version_bump.py(1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.8)
.github/workflows/bump-version.yml
196-196: could not parse as YAML: could not find expected ':'
(syntax-check)
🪛 Ruff (0.14.0)
scripts/ai_determine_version_bump.py
1-1: Shebang is present but file is not executable
(EXE001)
38-38: Starting a process with a partial executable path
(S607)
48-48: Consider moving this statement to an else block
(TRY300)
67-67: subprocess call: check for execution of untrusted input
(S603)
81-81: subprocess call: check for execution of untrusted input
(S603)
93-93: Consider moving this statement to an else block
(TRY300)
102-102: Avoid specifying long messages outside the exception class
(TRY003)
165-165: Avoid specifying long messages outside the exception class
(TRY003)
228-228: Avoid specifying long messages outside the exception class
(TRY003)
314-314: Consider moving this statement to an else block
(TRY300)
322-322: Do not catch blind exception: Exception
(BLE001)
333-333: Consider moving this statement to an else block
(TRY300)
341-341: Do not catch blind exception: Exception
(BLE001)
352-352: Consider moving this statement to an else block
(TRY300)
360-360: Do not catch blind exception: Exception
(BLE001)
439-439: Avoid specifying long messages outside the exception class
(TRY003)
447-447: Unpacked variable suffix is never used
Prefix it with an underscore or any other dummy variable pattern
(RUF059)
456-456: Avoid specifying long messages outside the exception class
(TRY003)
🪛 YAMLlint (1.37.1)
.github/workflows/bump-version.yml
[error] 198-198: syntax error: could not find expected ':'
(syntax)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy Docs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (5)
.github/workflows/bump-version.yml (3)
94-97: Workflow/script API mismatch:--provideris unsupported by the script.The script’s CLI lacks
--provider, causing an argparse error and job failure. Either add--providerto the script (preferred for the feature) or remove the flag here.Option A (keep feature — requires script change; see my script diff below).
Option B (quick unblock — remove the flag here):- python scripts/ai_determine_version_bump.py \ - --provider ${AI_PROVIDER} \ + python scripts/ai_determine_version_bump.py \ --output-format json \ - --verbose > /tmp/analysis.json 2>&1 + --verbose 1> /tmp/analysis.json 2> /tmp/analysis.log
97-101: Do not merge stderr into JSON output (breaks jq).Redirect stdout and stderr separately; also fail fast on parsing errors.
- --verbose > /tmp/analysis.json 2>&1 + --verbose 1> /tmp/analysis.json 2> /tmp/analysis.log + jq -e . /tmp/analysis.json >/dev/null
64-65: Guard againstnullfromgh pr listto avoid false positives.- EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "") + EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number // empty' || echo "")scripts/ai_determine_version_bump.py (2)
53-95: Parse commits robustly (multiline bodies) and add basic error checks.Current newline-based parsing drops bodies and can miss “BREAKING CHANGE” in %b.
-def get_commits_since_tag(tag: str) -> list[dict]: +def get_commits_since_tag(tag: str, max_commits: int = 100) -> list[dict]: @@ - if tag: - cmd = [ - "git", - "log", - f"{tag}..HEAD", - "--pretty=format:%H|||%s|||%b", - ] - else: - cmd = ["git", "log", "--pretty=format:%H|||%s|||%b"] - - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - - commits = [] - for line in result.stdout.strip().split("\n"): - if not line.strip(): - continue - parts = line.split("|||") - if len(parts) >= 2: - commit_hash = parts[0] - subject = parts[1] - body = parts[2] if len(parts) > 2 else "" - - # Get file changes for this commit - diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"] - diff_result = subprocess.run(diff_cmd, capture_output=True, text=True) - files_changed = diff_result.stdout.strip() - - commits.append( - { - "hash": commit_hash[:7], - "subject": subject, - "body": body, - "files_changed": files_changed, - } - ) + pretty = "%H%x1f%s%x1f%b%x1e" # \x1f=field sep, \x1e=record sep + base = ["git", "log", "--pretty=format:" + pretty, "-n", str(max(1, max_commits))] + cmd = base if not tag else ["git", "log", f"{tag}..HEAD", "--pretty=format:" + pretty, "-n", str(max(1, max_commits))] + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + commits: list[dict] = [] + for rec in result.stdout.split("\x1e"): + if not rec.strip(): + continue + fields = rec.split("\x1f") + if len(fields) < 2: + continue + commit_hash, subject = fields[0], fields[1] + body = fields[2] if len(fields) > 2 else "" + + diff_cmd = ["git", "diff", f"{commit_hash}^..{commit_hash}", "--stat"] + diff_result = subprocess.run(diff_cmd, capture_output=True, text=True, check=False) + files_changed = (diff_result.stdout or "").strip() + + commits.append( + { + "hash": commit_hash[:7], + "subject": subject, + "body": body, + "files_changed": files_changed, + } + )Optionally add a
--max-commitsCLI flag and plumb it through.
458-564: Add--providerCLI and honor preferred provider order (unblocks workflow).Aligns with workflow input and enables deterministic provider selection.
@@ -def analyze_with_ai(commits: list[dict], current_version: str) -> dict: +def analyze_with_ai(commits: list[dict], current_version: str, provider: str | None = None) -> dict: @@ - # Try OpenAI first + # Build preference order + order: list[str] = [] + if provider in {"openai", "anthropic", "gemini"}: + order.append(provider) + order += [p for p in ["openai", "anthropic", "gemini"] if p not in order] + # Try providers in order @@ -def main(): +def main(): @@ - parser = argparse.ArgumentParser( + parser = argparse.ArgumentParser( description="Use AI to determine semantic version bump type based on git commits" ) + parser.add_argument( + "--provider", + choices=["openai", "anthropic", "gemini"], + help="Preferred AI provider (overrides default order)", + ) @@ - result = analyze_with_ai(commits, current_version) + result = analyze_with_ai(commits, current_version, provider=args.provider)
🧹 Nitpick comments (5)
.github/workflows/auto-tag-and-release.yml (2)
64-75: Prefer GITHUB_TOKEN with actions: write (simpler secret management).Unless you require a PAT,
gh workflow runworks withGITHUB_TOKENwhenpermissions.actions: writeis set. If you keep the PAT, ensure it has the “workflow” scope.- env: - GH_TOKEN: ${{ secrets.FLASHINFER_BOT_TOKEN }} + env: + GH_TOKEN: ${{ github.token }}Please confirm
release.ymlsupportsworkflow_dispatchwith an inputtag. If not, this step will fail.
77-92: Fix summary link to release workflow (relative URL is brittle).- echo "🔗 [View Release Workflow Runs](../../actions/workflows/release.yml)" >> $GITHUB_STEP_SUMMARY + echo "🔗 [View Release Workflow Runs](https://github.com/${{ github.repository }}/actions/workflows/release.yml)" >> $GITHUB_STEP_SUMMARY.github/workflows/bump-version.yml (1)
41-50: Shell safety in long run blocks.Add
set -euo pipefailto fail fast on errors and undefined vars in critical steps (Install, Analyze, Generate PR, Create PR).- name: Install dependencies run: | + set -euo pipefail python -m pip install --upgrade pip pip install google-generativeai openai anthropic(Apply similarly to other multi-line run: blocks.)
scripts/ai_determine_version_bump.py (2)
444-456: Unusedsuffixfromparse_version; make intent explicit.- major, minor, patch, suffix = parse_version(current_version) + major, minor, patch, _suffix = parse_version(current_version)
229-236: Avoid experimental model; use stable production default.The code uses
gemini-2.0-flash-exp, which is experimental. For production usage, Google recommendsgemini-2.5-pro,gemini-2.5-flash, orgemini-2.0-flash-001. Replace the "-exp" suffix with a stable variant, such asgemini-2.0-flash-001or prefer the latest stablegemini-2.5-flash.Also applies to: 269-276
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
- .github/workflows/auto-tag-and-release.yml(1 hunks)
- .github/workflows/bump-version.yml(1 hunks)
- scripts/ai_determine_version_bump.py(1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.8)
.github/workflows/bump-version.yml
204-204: could not parse as YAML: could not find expected ':'
(syntax-check)
🪛 Ruff (0.14.0)
scripts/ai_determine_version_bump.py
1-1: Shebang is present but file is not executable
(EXE001)
38-38: Starting a process with a partial executable path
(S607)
48-48: Consider moving this statement to an else block
(TRY300)
66-66: subprocess call: check for execution of untrusted input
(S603)
80-80: subprocess call: check for execution of untrusted input
(S603)
92-92: Consider moving this statement to an else block
(TRY300)
101-101: Avoid specifying long messages outside the exception class
(TRY003)
164-164: Avoid specifying long messages outside the exception class
(TRY003)
227-227: Avoid specifying long messages outside the exception class
(TRY003)
313-313: Consider moving this statement to an else block
(TRY300)
321-321: Do not catch blind exception: Exception
(BLE001)
332-332: Consider moving this statement to an else block
(TRY300)
340-340: Do not catch blind exception: Exception
(BLE001)
351-351: Consider moving this statement to an else block
(TRY300)
359-359: Do not catch blind exception: Exception
(BLE001)
438-438: Avoid specifying long messages outside the exception class
(TRY003)
446-446: Unpacked variable suffix is never used
Prefix it with an underscore or any other dummy variable pattern
(RUF059)
455-455: Avoid specifying long messages outside the exception class
(TRY003)
🪛 YAMLlint (1.37.1)
.github/workflows/bump-version.yml
[error] 206-206: syntax error: could not find expected ':'
(syntax)
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: main | ||
| fetch-depth: 0 | ||
|  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetch tags to make tag-existence check reliable.
Without fetching tags, local git may miss remote tags. Fetch tags explicitly.
       uses: actions/checkout@v4
       with:
         ref: main
-        fetch-depth: 0
+        fetch-depth: 0
+        fetch-tags: true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| fetch-tags: true | 
🤖 Prompt for AI Agents
.github/workflows/auto-tag-and-release.yml around lines 19-23: the workflow
currently checks out the repo but doesn't guarantee remote tags are present
locally; update the checkout step to explicitly fetch tags (or add a short step
immediately after checkout to run a git fetch --tags) so subsequent
tag-existence checks are reliable. Ensure the change either enables the checkout
input that fetches tags or adds a dedicated git fetch tags step.
| - name: Check if tag already exists | ||
| id: check-tag | ||
| run: | | ||
| TAG="v${{ steps.get-version.outputs.version }}" | ||
| if git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| echo "exists=true" >> $GITHUB_OUTPUT | ||
| echo "⚠️ Tag $TAG already exists" | ||
| else | ||
| echo "exists=false" >> $GITHUB_OUTPUT | ||
| echo "✓ Tag $TAG does not exist yet" | ||
| fi | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check tag existence on remote, not only locally.
git rev-parse can miss remote tags and lead to duplicate tag attempts.
-      - name: Check if tag already exists
+      - name: Check if tag already exists (remote)
         id: check-tag
         run: |
           TAG="v${{ steps.get-version.outputs.version }}"
-          if git rev-parse "$TAG" >/dev/null 2>&1; then
+          if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then
             echo "exists=true" >> $GITHUB_OUTPUT
             echo "⚠️  Tag $TAG already exists"
           else
             echo "exists=false" >> $GITHUB_OUTPUT
             echo "✓ Tag $TAG does not exist yet"
           fi🤖 Prompt for AI Agents
.github/workflows/auto-tag-and-release.yml around lines 33 to 43: the current
check uses git rev-parse which only inspects local refs and can miss tags that
exist on the remote; update the step to check the remote (either run git fetch
--tags --no-recurse-submodules first, then test with git rev-parse, or call git
ls-remote --tags origin "refs/tags/$TAG" and treat a non-empty result as
existing) and write exists=true/false to $GITHUB_OUTPUT accordingly, preserving
the existing log messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
.github/workflows/bump-version.yml (2)
64-64: jq returns literal "null" string when no PR exists, causing false positive.When no open version-bump PR exists,
.[0]evaluates tonull, and.numberonnullreturns the string"null", not an empty string. The shell test[ -n "$EXISTING_PR" ]treats"null"as non-empty, incorrectly detecting an existing PR.Apply this diff:
- EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number' || echo "") + EXISTING_PR=$(gh pr list --state open --label "version-bump" --json number --jq '.[0].number // empty' || echo "")
96-97: Verbose logs written to stderr will corrupt JSON output.The
--verboseflag likely writes logs to stderr, and2>&1merges stderr into stdout before redirecting to/tmp/analysis.json. This corrupts the JSON, causingjqcommands on lines 100-102 to fail.Apply this diff to separate JSON from logs:
# Run AI analysis with specified provider python scripts/ai_determine_version_bump.py \ --provider ${AI_PROVIDER} \ --output-format json \ - --verbose > /tmp/analysis.json 2>&1 + --verbose > /tmp/analysis.json 2> /tmp/analysis.log + + echo "Verbose logs written to /tmp/analysis.log"
🧹 Nitpick comments (2)
scripts/generate_bump_pr_body.py (2)
1-1: Make script executable to match shebang.The shebang is present but the file isn't executable. While the workflow explicitly calls
python scripts/generate_bump_pr_body.py, making it executable improves portability and follows best practices.Run this command to fix:
chmod +x scripts/generate_bump_pr_body.py
4-24: Use explicitOptionaltype hints for parameters withNonedefaults.PEP 484 requires explicit
Optional[T]for parameters defaulting toNone. Current implicit optionals (lines 21-24) should be made explicit.Apply this diff:
import argparse import sys from pathlib import Path +from typing import Optional def read_file_safe(filepath: Path) -> str: @@ -17,10 +18,10 @@ def generate_pr_body( current_version: str, new_version: str, bump_type: str, - ai_provider: str = None, - reasoning_file: Path = None, - key_changes_file: Path = None, - run_id: str = None, + ai_provider: Optional[str] = None, + reasoning_file: Optional[Path] = None, + key_changes_file: Optional[Path] = None, + run_id: Optional[str] = None, ) -> str:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- .github/workflows/bump-version.yml(1 hunks)
- scripts/generate_bump_pr_body.py(1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.8)
.github/workflows/bump-version.yml
11-11: string should not be empty
(syntax-check)
🪛 Ruff (0.14.0)
scripts/generate_bump_pr_body.py
1-1: Shebang is present but file is not executable
(EXE001)
21-21: PEP 484 prohibits implicit Optional
Convert to Optional[T]
(RUF013)
22-22: PEP 484 prohibits implicit Optional
Convert to Optional[T]
(RUF013)
23-23: PEP 484 prohibits implicit Optional
Convert to Optional[T]
(RUF013)
24-24: PEP 484 prohibits implicit Optional
Convert to Optional[T]
(RUF013)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy Docs
🔇 Additional comments (1)
.github/workflows/bump-version.yml (1)
185-185: No issues found—script handles emptyai_providergracefully.Verification confirms that
generate_bump_pr_body.pysuccessfully handles emptyai_providervalues. When the parameter is empty, the script falls back to "Manual version bump" text and produces a complete, valid PR body without errors.
| python scripts/ai_determine_version_bump.py \ | ||
| --provider ${AI_PROVIDER} \ | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that the Python script accepts the --provider parameter.
The workflow passes --provider ${AI_PROVIDER} to scripts/ai_determine_version_bump.py. If the script doesn't accept this argument, it will fail with "unrecognized arguments" error.
Run this script to verify the parameter is supported:
🏁 Script executed:
#!/bin/bash
# Check if ai_determine_version_bump.py accepts --provider
if [ -f scripts/ai_determine_version_bump.py ]; then
  python scripts/ai_determine_version_bump.py --help | grep -E '\-\-provider'
  if [ $? -eq 0 ]; then
    echo "✓ Script accepts --provider flag"
  else
    echo "✗ Script does NOT accept --provider flag"
  fi
else
  echo "✗ Script file not found"
fiLength of output: 40
Now let me examine the actual Python script and workflow file directly:
🏁 Script executed:
#!/bin/bash
# Check the Python script and workflow file
echo "=== Checking Python script ==="
if [ -f scripts/ai_determine_version_bump.py ]; then
  head -100 scripts/ai_determine_version_bump.py
else
  echo "Script not found"
fi
echo ""
echo "=== Checking workflow context (lines 70-120) ==="
if [ -f .github/workflows/bump-version.yml ]; then
  sed -n '70,120p' .github/workflows/bump-version.yml
else
  echo "Workflow not found"
fiLength of output: 5241
Let me view the complete Python script to see the argparse definitions:
🏁 Script executed:
#!/bin/bash
# View the full Python script to check argparse setup
cat scripts/ai_determine_version_bump.py | grep -A 50 "argparse\|ArgumentParser\|add_argument" | head -80Length of output: 2139
Remove unsupported --provider flag; script auto-detects provider from environment variables.
The Python script does not accept --provider argument. It determines the AI provider by checking which API key environment variables are set (GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY) and tries them in order: OpenAI → Claude → Gemini → Fallback. Remove the --provider ${AI_PROVIDER} flag from line 95.
Additionally, the 2>&1 redirection on line 97 will mix --verbose stderr output with stdout JSON, corrupting the JSON that jq parses on lines 100-102. Redirect only stdout to the file: > /tmp/analysis.json (without 2>&1).
🤖 Prompt for AI Agents
In .github/workflows/bump-version.yml around lines 94–102, the workflow passes
an unsupported --provider ${AI_PROVIDER} flag to the Python script and redirects
stderr into stdout (2>&1), which corrupts the JSON consumed by jq; remove the
entire --provider ${AI_PROVIDER} argument from the python
scripts/ai_determine_version_bump.py invocation and change the output
redirection so only stdout is written to /tmp/analysis.json (remove the 2>&1),
leaving stderr on the console so jq can safely parse the JSON file.
| [ | ||
| "### Semantic Versioning Rules", | ||
| "", | ||
| "According to our [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md):", | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relative links with ../ don't work in GitHub PR bodies; repository info needed.
Markdown links in GitHub PR descriptions require absolute URLs or repository-relative paths. The current ../blob/main/... and ../actions/... patterns don't resolve correctly because there's no "parent" of the repository context.
To fix, the script needs the repository name (owner/repo) passed as a parameter to construct proper URLs like:
- https://github.com/{owner}/{repo}/blob/main/CONTRIBUTING.md
- https://github.com/{owner}/{repo}/actions/workflows/auto-tag-and-release.yml
- https://github.com/{owner}/{repo}/actions/runs/{run_id}
Apply this diff to add repository parameter and fix links:
 def generate_pr_body(
     current_version: str,
     new_version: str,
     bump_type: str,
+    repository: str,
     ai_provider: Optional[str] = None,
     reasoning_file: Optional[Path] = None,
     key_changes_file: Optional[Path] = None,
     run_id: Optional[str] = None,
 ) -> str:
     """Generate the PR body markdown text.
 
     Args:
         current_version: Current version string (e.g., "2.1.4")
         new_version: New version string (e.g., "2.2.0")
         bump_type: Type of version bump ("major", "minor", "patch")
+        repository: GitHub repository in 'owner/repo' format
         ai_provider: Optional AI provider name used for analysis
         reasoning_file: Optional path to file containing AI reasoning
         key_changes_file: Optional path to file containing key changes (one per line)
         run_id: Optional GitHub Actions run ID
 
     Returns:
         Formatted PR body as markdown string
     """
     bump_type_upper = bump_type.upper()
+    base_url = f"https://github.com/{repository}"
 
     # Build AI provider text
     ai_provider_text = f" (using {ai_provider})" if ai_provider else ""
@@ -79,7 +82,7 @@ def generate_pr_body(
     lines.extend(
         [
             "### Semantic Versioning Rules",
             "",
-            "According to our [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md):",
+            f"According to our [CONTRIBUTING.md]({base_url}/blob/main/CONTRIBUTING.md):",
             "",
             "- **Major**: Incompatible API changes",
             "- **Minor**: Added functionality that is backwards-compatible",
@@ -89,9 +92,9 @@ def generate_pr_body(
             "",
             "After merging this PR:",
-            "- The [auto-tag-and-release workflow](../actions/workflows/auto-tag-and-release.yml) will automatically:",
+            f"- The [auto-tag-and-release workflow]({base_url}/actions/workflows/auto-tag-and-release.yml) will automatically:",
             f"  1. Create git tag `v{new_version}` on the main branch",
-            "  2. Trigger the [release workflow](../actions/workflows/release.yml) to build and publish the release",
+            f"  2. Trigger the [release workflow]({base_url}/actions/workflows/release.yml) to build and publish the release",
             "",
             "---",
             "",
@@ -101,7 +104,7 @@ def generate_pr_body(
     # Add auto-generated footer
     if run_id:
         lines.append(
-            f"🤖 Auto-generated by [bump-version workflow](../actions/runs/{run_id})"
+            f"🤖 Auto-generated by [bump-version workflow]({base_url}/actions/runs/{run_id})"
         )
     else:
         lines.append("🤖 Auto-generated by bump-version workflow")
@@ -112,6 +115,7 @@ def main():
     parser = argparse.ArgumentParser(
         description="Generate PR body for version bump pull requests"
     )
     parser.add_argument(
         "--current-version", required=True, help="Current version (e.g., 2.1.4)"
     )
@@ -125,6 +129,10 @@ def main():
         choices=["major", "minor", "patch"],
         help="Type of version bump",
     )
+    parser.add_argument(
+        "--repository",
+        required=True,
+        help="GitHub repository in 'owner/repo' format",
+    )
     parser.add_argument(
         "--ai-provider", help="AI provider used for analysis (optional)"
     )
@@ -146,6 +154,7 @@ def main():
     # Generate PR body
     pr_body = generate_pr_body(
         current_version=args.current_version,
         new_version=args.new_version,
         bump_type=args.bump_type,
+        repository=args.repository,
         ai_provider=args.ai_provider,
         reasoning_file=args.reasoning_file,
         key_changes_file=args.key_changes_file,Then update the workflow at line 181-189 to pass the repository:
           python scripts/generate_bump_pr_body.py \
             --current-version "${{ steps.current-version.outputs.version }}" \
             --new-version "${{ steps.final-decision.outputs.new_version }}" \
             --bump-type "${{ steps.final-decision.outputs.bump_type }}" \
+            --repository "${{ github.repository }}" \
             --ai-provider "${{ steps.analyze.outputs.ai_provider }}" \
             --reasoning-file /tmp/reasoning.txt \
             --key-changes-file /tmp/key_changes.txt \
             --run-id "${{ github.run_id }}" \
             --output /tmp/pr_body.mdAlso applies to: 92-94, 104-104
🤖 Prompt for AI Agents
In scripts/generate_bump_pr_body.py around line 83 (and also addresses
occurrences at 92-94 and 104), replace repository-relative "../..." links with
absolute GitHub URLs by adding a new parameter for the repository full name
(owner/repo) to the script function/CLI, then construct links like
"https://github.com/{repo}/blob/main/CONTRIBUTING.md",
"https://github.com/{repo}/actions/workflows/auto-tag-and-release.yml" and
workflow run links using the run_id; update all occurrences that currently use
"../blob/..." or "../actions/..." to use these constructed URLs. Also update the
workflow invocation (around lines 181-189) to pass the repository full name into
the script (or workflow input) so the script can build those absolute links.
| name: Auto Tag and Release | ||
|  | ||
| on: | ||
| pull_request: | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be careful with automatically tagging. We want to make sure the tagged commit has a clean internal CI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could read a summary of the CI report and map it to the commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The automatic tagging seems to happen if and only if a PR with a version-bump label gets merged, which should only get merged after the CI has been reviewed. The PR seems to remove the additional step of manually tagging & releasing.
Usually a version bump PR like 1921 is more of an empty PR that updates version.txt -- the internal CI should have been triggered on the PR and the report should have already been reviewed before checking the PR in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but I believe the thinking is that we can have PRs in between the version bump and the actual tagged version, for agility reasons.
| return [] | ||
|  | ||
|  | ||
| def analyze_with_openai(commits_summary: str, current_version: str) -> dict: | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would provide some more descriptive names for these analyze_* functions. Like version_bump_from_commit_analysis_openai.
|  | ||
| client = OpenAI(api_key=api_key) | ||
|  | ||
| prompt = f"""You are analyzing git commits for a CUDA kernel library called FlashInfer to determine the appropriate semantic version bump. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prompt is duplicated across the different models. I would suggest to assign it to a variable, or put it into a separate file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually these functions all look very similar. Maybe we can just have one function and pass in the model as a paramerer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR has a lot of good ideas. I made some suggestions to improve the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that this is a very interesting PR. Left a few comments
| name: Auto Tag and Release | ||
|  | ||
| on: | ||
| pull_request: | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The automatic tagging seems to happen if and only if a PR with a version-bump label gets merged, which should only get merged after the CI has been reviewed. The PR seems to remove the additional step of manually tagging & releasing.
Usually a version bump PR like 1921 is more of an empty PR that updates version.txt -- the internal CI should have been triggered on the PR and the report should have already been reviewed before checking the PR in.
| return fallback_analysis(commits) | ||
|  | ||
|  | ||
| def fallback_analysis(commits: list[dict]) -> dict: | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback feels quite forced with regex matches. I wonder whether we realistically believe that a version bump PR opened based on this fallback logic will be any useful. It will require manual intervention to look at the actual commits that have been made.
| """ | ||
| Use AI to analyze commits and determine version bump. | ||
|  | ||
| Tries providers in order: OpenAI -> Claude -> Gemini -> Fallback | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the cost of the API calls are not expensive, would the quality of response be better with an ensemble method?
i.e. we make analyze with all three AIs (or up to three if not available), and make them vote on the best version number to bump to
| }} | ||
|  | ||
| Important considerations: | ||
| - Internal refactoring, test updates, documentation changes alone don't warrant a version bump | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also add benchmark code updates in list of things that don't warrant a version bump?
📌 Description
Add a new GitHub Actions workflow that automatically determines semantic version bumps using Google Gemini AI to analyze commit messages.
🔍 Related Issues
🚀 Pull Request Checklist
Thank you for contributing to FlashInfer! Before we review your pull request, please make sure the following items are complete.
✅ Pre-commit Checks
pre-commitby runningpip install pre-commit(or used your preferred method).pre-commit install.pre-commit run --all-filesand fixed any reported issues.🧪 Tests
unittest, etc.).Reviewer Notes
Summary by CodeRabbit