Skip to content

Conversation

@yzh119
Copy link
Collaborator

@yzh119 yzh119 commented Oct 19, 2025

📌 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

  • I have installed pre-commit by running pip install pre-commit (or used your preferred method).
  • I have installed the hooks with pre-commit install.
  • I have run the hooks manually with pre-commit run --all-files and fixed any reported issues.

If you are unsure about how to set up pre-commit, see the pre-commit documentation.

🧪 Tests

  • Tests have been added or updated as needed.
  • All tests are passing (unittest, etc.).

Reviewer Notes

Summary by CodeRabbit

  • Chores
    • Added an automated version-bump workflow (manual or scheduled) that analyzes recent commits, proposes semantic version bumps, includes AI reasoning and key changes in PRs, supports manual override, and prevents duplicate bump PRs.
  • New Features
    • Added an automated tagging and release workflow that creates a tag and triggers a release when a merged PR carries the version-bump label.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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

  • Automated Semantic Version Bumping: Introduces a new Python script (scripts/ai_determine_version_bump.py) designed to automatically determine the appropriate semantic version bump (major, minor, patch, or none) based on git commit messages.
  • AI-Powered Analysis: Leverages the Google Gemini AI API to perform intelligent analysis of commit subjects, bodies, and file changes, guided by the project's semantic versioning rules defined in CONTRIBUTING.md.
  • Robust Fallback Mechanism: Includes a fallback analysis method that uses simple keyword matching in commit messages if the GEMINI_API_KEY is not set, the google-generativeai package is not installed, or if there's an error calling the Gemini API.
  • Flexible Configuration: The script supports command-line arguments for specifying the current version, the git tag to analyze commits since, the maximum number of commits to consider, and the desired output format (simple or JSON).
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/bump-version.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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

Cohort / File(s) Summary
Version bump workflow
\.github/workflows/bump-version.yml
New workflow (manual or weekly) that checks out code, sets up Python, installs AI libs, reads version.txt, checks for an existing version-bump PR, runs scripts/ai_determine_version_bump.py (or applies a manual override), updates version.txt if needed, generates a PR body, and creates a PR via peter-evans/create-pull-request.
Auto tag & release workflow
\.github/workflows/auto-tag-and-release.yml
New workflow triggered on merged PRs to main with the version-bump label; reads version.txt, computes tag v<version>, checks for existing tag, creates and pushes an annotated tag if missing, and triggers a release via gh CLI; skips if tag exists.
AI bump determination script
scripts/ai_determine_version_bump.py
New Python CLI that obtains latest tag and commits, attempts AI analysis via OpenAI / Anthropic / Google Gemini (provider-specific functions with sequential fallbacks), falls back to keyword heuristics if needed, normalizes results to JSON (bump_type, reasoning, key_changes), and exposes helpers: get_latest_tag, get_commits_since_tag, analyze_with_*, fallback_analysis, parse_version, bump_version, and main.
PR body generator
scripts/generate_bump_pr_body.py
New script providing read_file_safe and generate_pr_body (and main) to build a Markdown PR body from current/new version, bump type, optional AI provider, reasoning and key changes files, and optional run ID; writes to stdout or file.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I sniff the commits in the night,
AI nudges which bits to bite.
A version hops, a PR takes flight,
Tags wait patient for the merge-light.
🥕 Release carrots gleam so bright.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "chore: agentic workflow for automatic version bump" is fully related to the main changes in this pull request. The changeset introduces a GitHub Actions workflow system for automated semantic version bumping using AI analysis, which the title accurately summarizes. The title is concise, avoids vague terminology, and clearly conveys the primary objective—adding an agentic workflow to automate version bumping. The "chore:" prefix is appropriately applied for infrastructure and tooling changes.
Description Check ✅ Passed The PR description follows the repository's template structure and includes all required sections: a clear Description section explaining that the PR adds a GitHub Actions workflow for automatic version bumping using AI, a Related Issues section (though empty), and the Pull Request Checklist with Pre-commit Checks and Tests subsections. While the description could be more comprehensive—it focuses on the workflow and Google Gemini AI but doesn't detail the additional Python scripts or multiple AI provider fallbacks—it provides sufficient core information about the main purpose and follows the expected format. The unchecked checklist items suggest the contributor may not have completed those steps, but the template structure itself is complete.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines 42 to 84
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 []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.run call to get the diff does not have check=True, which means errors will be silently ignored.
  • The current implementation makes a separate subprocess call 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 []

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: Handle git diff errors explicitly.

Use check=True or guard the output; current code ignores failures silently.

You can keep check=False but default files_changed to "" (as in the diff in my earlier comment). Ensure this is intentional.


249-258: Minor: unused suffix and exception messages.

suffix from parse_version is unused in bump_version; also long f-strings in raise trigger style rule. Prefix to _suffix and 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 python3

Action: set executable bit, or drop the shebang if always invoked via python ....

.github/workflows/bump-version.yml (2)

170-217: Avoid sed substitution for multiline reasoning; build the body directly.

sed fails 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-generativeai can 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ffab81 and 01396dd.

📒 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.text as the standard accessor for extracting text from responses, and genai.types.GenerationConfig is the proper mechanism for passing generation configuration. Your existing code already follows these best practices. The suggested fallback to internal implementation details like response.candidates[0].content.parts[0].text is unnecessary and contradicts the SDK's documented public API contract.

The concern about version variance appears unfounded; the .text accessor is the stable, uniform interface across SDK versions.

Likely an incorrect or invalid review comment.

Comment on lines +55 to +63
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines 73 to 81
# 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
# 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.

Comment on lines 287 to 291
"--max-commits",
type=int,
default=100,
help="Maximum number of commits to analyze",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 01396dd and 8fdea5b.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: --provider is unsupported by the script.

The script’s CLI lacks --provider, causing an argparse error and job failure. Either add --provider to 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 against null from gh pr list to 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-commits CLI flag and plumb it through.


458-564: Add --provider CLI 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 run works with GITHUB_TOKEN when permissions.actions: write is 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.yml supports workflow_dispatch with an input tag. 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 pipefail to 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: Unused suffix from parse_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 recommends gemini-2.5-pro, gemini-2.5-flash, or gemini-2.0-flash-001. Replace the "-exp" suffix with a stable variant, such as gemini-2.0-flash-001 or prefer the latest stable gemini-2.5-flash.

Also applies to: 269-276

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdea5b and c482cc7.

📒 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)

Comment on lines +19 to +23
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +33 to +43
- 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 to null, and .number on null returns 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 --verbose flag likely writes logs to stderr, and 2>&1 merges stderr into stdout before redirecting to /tmp/analysis.json. This corrupts the JSON, causing jq commands 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 explicit Optional type hints for parameters with None defaults.

PEP 484 requires explicit Optional[T] for parameters defaulting to None. 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

📥 Commits

Reviewing files that changed from the base of the PR and between c482cc7 and cec8c7c.

📒 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 empty ai_provider gracefully.

Verification confirms that generate_bump_pr_body.py successfully handles empty ai_provider values. When the parameter is empty, the script falls back to "Manual version bump" text and produces a complete, valid PR body without errors.

Comment on lines +94 to +95
python scripts/ai_determine_version_bump.py \
--provider ${AI_PROVIDER} \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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"
fi

Length 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"
fi

Length 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 -80

Length 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):",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.md

Also 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:
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator

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.

Copy link
Contributor

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:
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor

@nvmbreughe nvmbreughe left a 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.

Copy link
Collaborator

@bkryu bkryu left a 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:
Copy link
Collaborator

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:
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants