Skip to content

ci: add LGTM Bot with merge readiness tracking and AI-powered CI diagnostics#6066

Closed
dagil-nvidia wants to merge 61 commits into
mainfrom
dagil/ci-lgtm-bot
Closed

ci: add LGTM Bot with merge readiness tracking and AI-powered CI diagnostics#6066
dagil-nvidia wants to merge 61 commits into
mainfrom
dagil/ci-lgtm-bot

Conversation

@dagil-nvidia

@dagil-nvidia dagil-nvidia commented Feb 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add LGTM Bot for PR merge readiness automation
  • Automatic lgtm label, live merge checklist, on-demand diagnostics with LLM-powered CI fix suggestions
  • Auto-request CODEOWNERS reviews, stale review nudges, and merge reminders
  • Designed to be equally useful for internal org members and external contributors

Note: Bot comments currently post as github-actions[bot]. A dedicated GitHub App (lgtm-bot[bot]) will be created once the bot proves useful.

Architecture

flowchart LR
    subgraph triggers[" "]
        A["PR opened/pushed"] --> B["lgtm-bot-pr-open"]
        C["CI completes / review changes"] --> D["lgtm-bot"]
        E["/lgtm-bot diagnose"] --> F["lgtm-bot-diagnose"]
        G["Daily schedule"] --> H["lgtm-bot-stale"]
    end

    B --> B1["Auto-request CODEOWNERS reviews"]
    B --> B2["Post required teams + /ok to test"]

    D --> D1["Evaluate all merge gates"]
    D1 --> D2{"All pass?"}
    D2 -- Yes --> D3["Add lgtm label + Slack"]
    D2 -- No --> D4["Update checklist"]

    F --> F1["Full diagnosis + LLM fix suggestions"]

    H --> H1["Nudge stale reviews (48h)"]
    H --> H2["Remind to merge (24h)"]
Loading

Details

New Workflows

Workflow Trigger What it does
lgtm-bot.yml CI completion, review changes, push Evaluates merge readiness, manages lgtm label, maintains live checklist
lgtm-bot-diagnose.yml /lgtm-bot diagnose comment Runs diagnosis: CI, reviews, CODEOWNERS, conversations, DCO, conflicts. LLM fix suggestions via NVIDIA API
lgtm-bot-pr-open.yml PR open/synchronize Auto-requests CODEOWNERS reviews, posts visibility comment with /ok to test guidance
lgtm-bot-stale.yml Daily 9am PST (weekdays) Nudges stale reviews (48h+), reminds authors to merge (24h+ with lgtm)

New Script

  • .github/scripts/lgtm_bot_diagnose.py — Diagnosis engine with CODEOWNERS parsing, GitHub REST/GraphQL API, and NVIDIA LLM API for CI failure fix suggestions.

Updated

  • CONTRIBUTING.md — New "Merge Readiness & LGTM Bot" section. Clarifies /ok to test is per-commit and CODEOWNERS reviews are auto-requested.

Required Secrets/Variables

Name Type Purpose
NVIDIA_INFERENCE_API_KEY Secret LLM diagnosis (optional — degrades gracefully)
SLACK_LGTM_WEBHOOK_URL Secret Slack notification (optional)
SLACK_LGTM_ENABLED Variable Kill switch for Slack (optional)

Where should the reviewer start?

  1. .github/workflows/lgtm-bot.yml — Core label + checklist logic
  2. .github/scripts/lgtm_bot_diagnose.py — Diagnosis engine
  3. CONTRIBUTING.md — Documentation updates

Test Plan

  • CI passes
  • YAML syntax validated locally (yaml.safe_load)
  • Python syntax validated locally (py_compile)
  • CODEOWNERS parsing tested against real CODEOWNERS file
  • NVIDIA Inference API endpoint tested and working
  • GitHub API calls verified against live PR
  • Comment markers are unique across all workflows (5 distinct markers)

Summary by CodeRabbit

Release Notes

  • New Features

    • Automated merge readiness evaluation with live checklist on pull requests
    • Automatic reviewer requests based on code ownership rules
    • On-demand diagnostic analysis for merge-blocking issues
    • Automated error classification for CI failures with suggested solutions
    • Stale review reminders and merge readiness notifications
  • Documentation

    • Updated contribution guide with new automation features and slash commands

- Automatic `lgtm` label when all CI checks pass, PR is approved, and
  branch protection is satisfied; removed on new push
- Live merge checklist comment updated on every CI/review/push event
- On-demand `/lgtm-bot diagnose` command with LLM-powered CI failure
  diagnosis via NVIDIA Inference API (Claude Opus 4.6)
- Auto-request CODEOWNERS team reviews and post visibility comment on
  PR open with fork CI detection (/ok to test per-commit)
- PR size labels (XS/S/M/L/XL) based on lines changed
- Stale review nudge (48h) and merge reminder (24h with lgtm label)
- Slack notification when lgtm label is newly added (opt-in)
- Update CONTRIBUTING.md with merge readiness section and bot docs

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia
dagil-nvidia requested a review from a team February 8, 2026 20:52
@copy-pr-bot

copy-pr-bot Bot commented Feb 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/ok to test f8f4cde

@github-actions github-actions Bot added ci Issues/PRs that reference CI build/test documentation Improvements or additions to documentation actions labels Feb 8, 2026
Size labels (XS/S/M/L/XL/XXL) are already applied by an existing
integration. Remove the redundant Feature 5 logic from
lgtm-bot-pr-open.yml to avoid conflicts and label churn.

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/ok to test 43a992c

@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an LGTM bot: multiple GitHub Actions workflows, a diagnostics script, and an error-classification package to automate PR onboarding, CODEOWNERS review requests, live merge checklist and lgtm label management, stale nudges/merge reminders, and on-demand /diagnose diagnostics (optional LLM-assisted CI failure classification).

Changes

Cohort / File(s) Summary
Diagnostic Script
​.github/scripts/lgtm_bot_diagnose.py
New script implementing GitHub REST/GraphQL helpers, CODEOWNERS parsing/matching, CI aggregation (check-runs & statuses), failed-log retrieval, optional LLM classifier integration, review/unresolved-conversation analysis, and posting/updating a diagnostic Markdown comment.
Error Classification Package
error_classification/__init__.py, error_classification/classifier.py, error_classification/claude_client.py, error_classification/config.py, error_classification/pr_commentator.py, error_classification/prompts.py, error_classification/requirements.txt
New package implementing LLM client, rate limiting, prompts, config, classifier orchestration, PR comment formatting, and public exports for error classification and summarization.
Classification Scripts
​.github/scripts/classify_workflow_errors.py
New orchestration script that locates failed workflow runs, fetches job logs, runs error classification in parallel, aggregates results, and optionally posts PR comments.
Workflows — LGTM Bot & On-Demand
​.github/workflows/lgtm-bot.yml, ​.github/workflows/lgtm-bot-diagnose.yml, ​.github/workflows/lgtm-bot-pr-open.yml, ​.github/workflows/lgtm-bot-stale.yml
New workflows to maintain live merge checklist and lgtm label, handle PR open onboarding (CODEOWNERS review requests and summary comment), run scheduled stale nudges/merge reminders, and trigger on-demand /diagnose via comment.
Workflows — Error Classification Integration
​.github/workflows/classify-workflow-errors-reusable.yml, ​.github/workflows/build-frontend-image.yaml (job added), ​.github/workflows/ci-test-suite.yml (job added), ​.github/workflows/container-validation-dynamo.yml (job added), ​.github/workflows/copyright-checks.yml (job added), ​.github/workflows/pr.yaml (job added), ​.github/workflows/pre-merge.yml (job added)
Adds a reusable classify-workflow-errors workflow and integrates classify-errors jobs into multiple existing CI workflows to run post-job error classification and optional PR comments.
Docs & Config
.github/LGTM_BOT.md, CONTRIBUTING.md, .github/filters.yaml
New documentation for the LGTM Bot and error classifier; CONTRIBUTING.md updated with bot commands and behaviors; CI filters updated to ignore error_classification/**.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I nibble logs and chase the thread,
I map the teams where changes led.
I count the checks and fetch the clues,
Then whisper fixes in tidy views.
Hop on, PR—I'll help you through.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (91 files):

⚔️ .github/filters.yaml (content)
⚔️ .github/scripts/route_buildkit.sh (content)
⚔️ .github/workflows/build-frontend-image.yaml (content)
⚔️ .github/workflows/ci-test-suite.yml (content)
⚔️ .github/workflows/container-validation-dynamo.yml (content)
⚔️ .github/workflows/copyright-checks.yml (content)
⚔️ .github/workflows/pr.yaml (content)
⚔️ .github/workflows/pre-merge.yml (content)
⚔️ CONTRIBUTING.md (content)
⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ components/src/dynamo/frontend/main.py (content)
⚔️ components/src/dynamo/frontend/vllm_processor.py (content)
⚔️ components/src/dynamo/global_router/__main__.py (content)
⚔️ components/src/dynamo/mocker/main.py (content)
⚔️ components/src/dynamo/profiler/deploy/profile_sla_aic_dgdr.yaml (content)
⚔️ components/src/dynamo/profiler/deploy/profile_sla_dgdr.yaml (content)
⚔️ components/src/dynamo/profiler/deploy/profile_sla_moe_dgdr.yaml (content)
⚔️ components/src/dynamo/profiler/profile_sla.py (content)
⚔️ components/src/dynamo/profiler/utils/config_modifiers/protocol.py (content)
⚔️ components/src/dynamo/profiler/utils/defaults.py (content)
⚔️ components/src/dynamo/profiler/utils/estimate_perf.py (content)
⚔️ components/src/dynamo/profiler/utils/profiler_argparse.py (content)
⚔️ components/src/dynamo/profiler/utils/search_space_autogen.py (content)
⚔️ components/src/dynamo/sglang/args.py (content)
⚔️ components/src/dynamo/sglang/main.py (content)
⚔️ components/src/dynamo/sglang/register.py (content)
⚔️ components/src/dynamo/trtllm/workers/llm_worker.py (content)
⚔️ components/src/dynamo/trtllm/workers/video_diffusion_worker.py (content)
⚔️ components/src/dynamo/vllm/args.py (content)
⚔️ components/src/dynamo/vllm/backend_args.py (content)
⚔️ components/src/dynamo/vllm/handlers.py (content)
⚔️ components/src/dynamo/vllm/main.py (content)
⚔️ components/src/dynamo/vllm/tests/test_vllm_kv_events_api.py (content)
⚔️ components/src/dynamo/vllm/tests/test_vllm_unit.py (content)
⚔️ container/context.yaml (content)
⚔️ container/templates/args.Dockerfile (content)
⚔️ container/templates/vllm_runtime.Dockerfile (content)
⚔️ deploy/helm/charts/crds/templates/nvidia.com_dynamographdeploymentrequests.yaml (content)
⚔️ deploy/operator/api/v1alpha1/common.go (content)
⚔️ deploy/operator/api/v1alpha1/dynamographdeploymentrequest_types.go (content)
⚔️ deploy/operator/api/v1alpha1/zz_generated.deepcopy.go (content)
⚔️ deploy/operator/config/crd/bases/nvidia.com_dynamographdeploymentrequests.yaml (content)
⚔️ deploy/operator/config/samples/nvidia.com_v1alpha1_dynamographdeploymentrequest.yaml (content)
⚔️ deploy/operator/internal/controller/dynamographdeploymentrequest_controller.go (content)
⚔️ deploy/operator/internal/controller/dynamographdeploymentrequest_controller_test.go (content)
⚔️ deploy/operator/internal/dynamo/backend_trtllm.go (content)
⚔️ deploy/operator/internal/dynamo/backend_trtllm_test.go (content)
⚔️ deploy/operator/internal/webhook/validation/dynamographdeploymentrequest.go (content)
⚔️ deploy/operator/internal/webhook/validation/dynamographdeploymentrequest_test.go (content)
⚔️ docs/pages/components/frontend/README.md (content)
⚔️ docs/pages/components/frontend/frontend-guide.md (content)
⚔️ docs/pages/components/kvbm/kvbm-guide.md (content)
⚔️ docs/pages/components/profiler/profiler-guide.md (content)
⚔️ docs/pages/components/router/README.md (content)
⚔️ docs/pages/components/router/router-examples.md (content)
⚔️ docs/pages/components/router/router-guide.md (content)
⚔️ docs/pages/development/backend-guide.md (content)
⚔️ docs/pages/features/multimodal/multimodal-trtllm.md (content)
⚔️ docs/pages/features/multimodal/multimodal-vllm.md (content)
⚔️ docs/pages/integrations/kv-events-custom-engines.md (content)
⚔️ docs/pages/kubernetes/api-reference.md (content)
⚔️ examples/backends/tritonserver/src/tritonworker.py (content)
⚔️ examples/multimodal/components/processor.py (content)
⚔️ lib/async-openai/src/types/assistant.rs (content)
⚔️ lib/async-openai/src/types/chat.rs (content)
⚔️ lib/async-openai/src/types/completion.rs (content)
⚔️ lib/bindings/c/src/lib.rs (content)
⚔️ lib/bindings/kvbm/README.md (content)
⚔️ lib/bindings/python/examples/hello_world/server_sglang.py (content)
⚔️ lib/bindings/python/examples/hello_world/server_sglang_tok.py (content)
⚔️ lib/bindings/python/examples/hello_world/server_vllm.py (content)
⚔️ lib/bindings/python/rust/lib.rs (content)
⚔️ lib/bindings/python/rust/llm/kv.rs (content)
⚔️ lib/bindings/python/src/dynamo/_core.pyi (content)
⚔️ lib/bindings/python/src/dynamo/llm/__init__.py (content)
⚔️ lib/bindings/python/tests/test_tensor.py (content)
⚔️ lib/kv-router/src/protocols.rs (content)
⚔️ lib/llm/src/kv_router.rs (content)
⚔️ lib/llm/src/kv_router/prefill_router.rs (content)
⚔️ lib/llm/src/kv_router/publisher.rs (content)
⚔️ lib/llm/src/kv_router/push_router.rs (content)
⚔️ lib/llm/src/preprocessor/media/README.md (content)
⚔️ lib/llm/src/protocols/common/preprocessor.rs (content)
⚔️ lib/llm/src/protocols/openai/validate.rs (content)
⚔️ lib/runtime/examples/Cargo.lock (content)
⚔️ pyproject.toml (content)
⚔️ tests/frontend/grpc/echo_tensor_worker.py (content)
⚔️ tests/profiler/test_profile_sla_aiconfigurator.py (content)
⚔️ tests/profiler/test_profile_sla_dryrun.py (content)
⚔️ tests/serve/launch/template_verifier.py (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: adding LGTM Bot with merge readiness tracking and AI-powered CI diagnostics.
Description check ✅ Passed The PR description includes all required template sections: Overview, Details (with architecture diagram and workflow table), Where to start (reviewer guidance), and Related Issues placeholder with thorough documentation.
Docstring Coverage ✅ Passed Docstring coverage is 98.08% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In @.github/scripts/lgtm_bot_diagnose.py:
- Around line 652-661: The code currently assumes pr["head"]["repo"] is always
present and does is_fork = pr["head"]["repo"]["full_name"] != f"{OWNER}/{REPO}",
which crashes when pr["head"]["repo"] is None (deleted fork). Modify main() to
safely fetch head_repo = pr["head"].get("repo") (or use pr["head"]["repo"] and
check for None), then compute is_fork by treating a missing head_repo as a fork
(e.g., is_fork = head_repo is None or head_repo.get("full_name") !=
f"{OWNER}/{REPO}"), and use head_repo when needed instead of indexing
pr["head"]["repo"] directly.
- Around line 622-631: post_diagnostic_comment currently only requests the first
100 comments via github_request and can miss an existing DIAGNOSIS_MARKER when a
PR has >100 comments; update post_diagnostic_comment to paginate through
comments (e.g., loop requesting page=1..N with per_page=100 until no results) or
request comments sorted newest-first (add direction=desc or sort=created if
supported) and search each page for DIAGNOSIS_MARKER before creating a new
comment, using the existing github_request call and OWNER/REPO identifiers to
locate pages and stop when no further comments are returned.
- Around line 134-159: The unanchored matching branch in match_codeowners
currently only tests the basename with fnmatch, which fails when an unanchored
pattern contains '/' (e.g., "docs/*.md"); update the unanchored logic in
match_codeowners to: if the pattern contains a '/' use
pathlib.PurePath(filepath).match(pattern) to allow matching at any depth
(GitHub-style path matching), otherwise keep the existing filename-only fnmatch
against the basename; adjust the branch that computes filename so it only
applies to filename-only patterns and leave the anchored and directory branches
unchanged.

In @.github/workflows/lgtm-bot-diagnose.yml:
- Around line 42-61: The workflow's diagnose job currently triggers for any
commenter and exposes secrets/API quota; update the job "diagnose" condition to
gate execution to either (a) commenters who are repo collaborators/org members
by checking github.event.comment.author_association (e.g., allow MEMBER,
COLLABORATOR, OWNER) or (b) the PR author by allowing github.actor ==
github.event.issue.user.login, so only authorized users can reach the step that
runs .github/scripts/lgtm_bot_diagnose.py and uses
GITHUB_TOKEN/NVIDIA_INFERENCE_API_KEY; implement the chosen check in the job's
if expression to prevent untrusted comment triggers.

In @.github/workflows/lgtm-bot-pr-open.yml:
- Around line 62-63: The size label calculation currently uses the incoming pr
payload (totalChanges = pr.additions + pr.deletions) which can be stale for
synchronize events; update the logic to compute totalChanges from the freshly
fetched PR object you already retrieve for CODEOWNERS (use the re-fetched pr
response instead of the original payload) so SIZE_LABELS and sizeLabel are
derived from the up-to-date PR data (reference: the pr variable, totalChanges,
SIZE_LABELS, and sizeLabel).
- Around line 140-186: The matchFile function incorrectly handles unanchored
CODEOWNERS patterns that contain a slash by matching only the filename; update
matchFile so that when a pattern p (from rules) is not anchored but contains '/'
you test the full filepath (using globMatch(filepath, clean)) instead of
filepath.split('/').pop(), remove the redundant second globMatch(filepath,
clean) branch inside the anchored block, and ensure directory patterns
(clean.endsWith('/')) still respect anchored vs unanchored semantics; refer to
the globMatch and matchFile functions, rule.pattern (p), clean, anchored, and
rules to locate and fix the logic.

In @.github/workflows/lgtm-bot-stale.yml:
- Around line 88-133: The "daysWaiting" value is computed from prAge (derived
from pr.created_at) which can misrepresent how long requested reviewers have
actually been waiting; update the logic that sets daysWaiting to use the
timestamp of the most recent review request instead of pr.created_at: fetch the
PR timeline events (e.g., via github.rest.issues.listEvents or
issues.listEventsForTimeline) and find the latest event with type
"review_requested" or action indicating a review request, compute daysWaiting
from (now - new Date(latestReviewRequest.created_at)), and fall back to prAge
only if no review-request event exists; adjust any references to daysWaiting and
keep the existing NUDGE_MARKER/comment posting flow.
- Line 28: The cron comment is misleading because "9am PT (UTC-8)" only holds
during PST; update the comment next to the cron schedule string cron: '0 17 * *
1-5' to indicate DST, e.g. "9am PT (UTC-8 in standard time, UTC-7 in daylight
saving time)" or similar; locate the cron line cron: '0 17 * * 1-5' and replace
its trailing comment so it accurately reflects both PST and PDT.

In @.github/workflows/lgtm-bot.yml:
- Around line 244-263: The synchronize handler currently rebuilds the checklist
with hasApproval: false and hasChangesRequested: false which causes a transient
incorrect UI; update the branch inside the if (context.eventName ===
'pull_request_target') block (around REQUIRED_CHECKS, buildChecklist,
updateChecklist calls) to fetch current PR reviews and compute proper
hasApproval/hasChangesRequested before calling buildChecklist (or alternatively
pass a flag/message that approvals are being rechecked) so the checklist
reflects actual review state for prNumber/headSha/isFork instead of always
resetting approvals to false; ensure removeLabel remains unchanged and then call
updateChecklist with the computed values.
- Around line 154-162: The current heuristic treats a `pending` pmStatus as "not
triggered"; instead detect whether the check run exists and only show the
"maintainer must run" note when the check run is absent. Update the isFork
branch: compute a boolean like hasPm =
Object.prototype.hasOwnProperty.call(checks, 'pre-merge-status-check'), read
pmStatus = checks['pre-merge-status-check'], then set triggered = hasPm &&
pmStatus !== 'completed' (so present-but-pending/running counts as triggered),
and only include the note when !hasPm; keep using headSha.slice(0,7] and the
same lines.push code paths.
🧹 Nitpick comments (6)
.github/scripts/lgtm_bot_diagnose.py (3)

40-45: Module-level env reads will crash on import if vars are missing.

Lines 40 and 42 use os.environ["..."] which raises KeyError immediately if the env var is absent. This is acceptable for a script that's only invoked by a controlled workflow, but consider wrapping the reads in main() or adding a friendlier error message to aid debugging if the workflow configuration drifts.


536-540: Uses global PR_NUMBER instead of the pr parameter for building URLs.

build_diagnostic_comment receives pr as a parameter but references the module-level PR_NUMBER at line 538. This couples the function to global state and would break if it were ever called with a different PR number.

Proposed fix
-        lines.append(
-            f"- No approved reviews — "
-            f"[request a review]"
-            f"(https://github.com/{OWNER}/{REPO}/pull/{PR_NUMBER}"
-            f"/reviewers)"
-        )
+        pr_number = pr["number"]
+        lines.append(
+            f"- No approved reviews — "
+            f"[request a review]"
+            f"(https://github.com/{OWNER}/{REPO}/pull/{pr_number}"
+            f"/reviewers)"
+        )

97-111: GraphQL helper lacks error handling compared to REST helper.

github_graphql doesn't handle HTTPError (unlike github_request) and also doesn't check for GraphQL-level errors in the response payload. A GraphQL error (e.g., rate limit or schema issue) will return HTTP 200 with an errors key, silently producing bad data downstream.

Proposed fix — log GraphQL errors
 def github_graphql(query: str, variables: dict | None = None) -> Any:
     """Make a GitHub GraphQL API request."""
     payload = {"query": query}
     if variables:
         payload["variables"] = variables
 
     url = "https://api.github.com/graphql"
     headers = {
         "Authorization": f"Bearer {GITHUB_TOKEN}",
         "Content-Type": "application/json",
     }
     body = json.dumps(payload).encode()
     req = urllib.request.Request(url, data=body, headers=headers, method="POST")
-    with urllib.request.urlopen(req) as resp:
-        return json.loads(resp.read().decode())
+    try:
+        with urllib.request.urlopen(req) as resp:
+            result = json.loads(resp.read().decode())
+            if "errors" in result:
+                print(f"GraphQL errors: {result['errors']}", file=sys.stderr)
+            return result
+    except urllib.error.HTTPError as e:
+        error_body = ""
+        try:
+            error_body = e.read().decode()
+        except Exception:
+            pass
+        print(f"GraphQL API error: {e.code} {e.reason} {error_body}", file=sys.stderr)
+        raise
.github/workflows/lgtm-bot-stale.yml (1)

72-133: Each PR triggers at least one API call for listRequestedReviewers — potential rate-limit concern.

For repos with hundreds of open PRs, this loop makes O(N) API calls minimum. GitHub Actions GITHUB_TOKEN has a rate limit of 1,000 requests/hour for the REST API. Consider filtering PRs by age first (skip recently updated PRs) or adding an early continue if pr.updated_at is recent enough that the review can't be stale.

.github/workflows/lgtm-bot-pr-open.yml (2)

253-259: teamReason relies on keyword matching in team names — fragile.

If team names don't contain 'rust', 'python', 'Devops', or 'deploy', the fallback is the generic "Files changed." This won't provide useful information. Also note the inconsistent casing ('Devops' vs lowercase others).

Consider deriving the reason from file extensions or paths instead, which is more robust.

Proposed alternative: derive reason from files
-            function teamReason(team) {
-              if (team.includes('rust')) return 'Rust files changed';
-              if (team.includes('python')) return 'Python files changed';
-              if (team.includes('Devops')) return 'CI/infra files changed';
-              if (team.includes('deploy')) return 'Deploy files changed';
-              return 'Files changed';
-            }
+            function teamReason(team, files) {
+              const exts = files.map(f => f.split('.').pop()).filter(Boolean);
+              if (exts.some(e => ['rs', 'toml'].includes(e))) return 'Rust files changed';
+              if (exts.some(e => ['py', 'pyi'].includes(e))) return 'Python files changed';
+              if (files.some(f => f.startsWith('.github/') || f.startsWith('ci/'))) return 'CI/infra files changed';
+              if (files.some(f => f.startsWith('deploy/'))) return 'Deploy files changed';
+              return 'Files changed';
+            }

And update the call site:

-              commentLines.push(`| ${team} | ${teamReason(team)} | ${fileList}${more} |`);
+              commentLines.push(`| ${team} | ${teamReason(team, files)} | ${fileList}${more} |`);

25-31: pull_request_target with synchronize runs on every push — consider the cost.

This workflow runs CODEOWNERS parsing, label management, and potentially review requests on every push to a PR. For active PRs with frequent pushes, the CODEOWNERS comment updates and size label changes are redundant if files haven't changed. This is not a blocker but worth noting for optimization later.

Comment thread .github/scripts/lgtm_bot_diagnose.py
Comment thread .github/scripts/lgtm_bot_diagnose.py Outdated
Comment thread .github/scripts/lgtm_bot_diagnose.py Outdated
Comment thread .github/workflows/lgtm-bot-diagnose.yml Outdated
Comment thread .github/workflows/lgtm-bot-pr-open.yml Outdated
Comment thread .github/workflows/lgtm-bot-pr-open.yml
Comment thread .github/workflows/lgtm-bot-stale.yml Outdated
Comment thread .github/workflows/lgtm-bot-stale.yml
Comment thread .github/workflows/lgtm-bot.yml Outdated
Comment thread .github/workflows/lgtm-bot.yml
- Fix crash when fork repo is deleted (head.repo can be None)
- Fix CODEOWNERS matching for unanchored patterns with path separators
  (both Python and JS implementations)
- Add auth gate on /lgtm-bot diagnose (block only NONE association,
  allow all contributors including first-timers)
- Paginate diagnostic comment search from newest first
- De-fork /ok to test guidance: show CI trigger instructions for ALL
  PRs, not just forks (internal members without GPG also need it)
- Improve CI trigger heuristic: check if check run exists rather than
  assuming pending means not triggered
- Fetch reviews in synchronize path to keep checklist accurate
- Fix stale nudge days calculation (use last update, not PR age)
- Fix cron comment (PST/PDT clarification)

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/ok to test f1fb525

@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/scripts/lgtm_bot_diagnose.py:
- Around line 239-266: The fallback that inspects commit statuses (the
status_match from combined.get("statuses")) wrongly treats any non-"success"
state as a failure; update the mapping in the block that builds results[name] so
that only explicit failure states (e.g., "failure", "error", "cancelled" or
"closed") append to failing and set "status":"fail", while transient states like
"pending" (and possibly "queued" or "in_progress") should set results[name] =
{"status":"pending", "url": ...} instead of "fail"; ensure you only append to
the failing list for the explicit failure states and preserve the original
target_url extraction.

In @.github/workflows/lgtm-bot-pr-open.yml:
- Around line 252-258: The current call to github.rest.issues.listComments (used
to find the CODEOWNERS_MARKER comment) only fetches a single page with per_page:
100 and can miss older comments on very active PRs; update the lookup to
paginate through all pages (or request from newest) using Octokit pagination
(e.g., octokit.paginate or passing page params in a loop) when retrieving
comments into the comments variable so the search for CODEOWNERS_MARKER reliably
finds existing bot comments and prevents duplicates.

In @.github/workflows/lgtm-bot.yml:
- Around line 116-139: The updateChecklist function currently calls
github.rest.issues.listComments with per_page: 100 which only fetches the first
page and can miss the existing checklist comment; modify updateChecklist to
paginate through all comment pages (or request newest-first and stop early) when
calling github.rest.issues.listComments so you reliably find the comment
containing CHECKLIST_MARKER before deciding to update or create; keep the
existing updateComment/createComment logic (references: updateChecklist,
CHECKLIST_MARKER, github.rest.issues.listComments,
github.rest.issues.updateComment, github.rest.issues.createComment).
- Around line 330-344: The fallback that maps commit status in
combinedStatus.statuses (found as statusMatch) currently treats any
non-'success' state as 'fail'; update the conditional around statusMatch.state
in this block to distinguish 'pending' explicitly: set checkResults[name] =
'pass' for 'success', set checkResults[name] = 'pending' for 'pending' (and
ensure allChecksPassing = false), and set checkResults[name] = 'fail' for other
states (also setting allChecksPassing = false); modify the logic around
statusMatch, checkResults, and allChecksPassing accordingly so the checklist
shows running for pending statuses rather than marking them failing.
🧹 Nitpick comments (4)
.github/workflows/lgtm-bot-pr-open.yml (1)

213-220: teamReason heuristic uses case-sensitive substring matching — may miss teams.

team.includes('Devops') is case-sensitive. If the team slug is devops or DevOps, this won't match. Consider using team.toLowerCase().includes('devops') for robustness.

Suggested fix
             function teamReason(team) {
-              if (team.includes('rust')) return 'Rust files changed';
-              if (team.includes('python')) return 'Python files changed';
-              if (team.includes('Devops')) return 'CI/infra files changed';
-              if (team.includes('deploy')) return 'Deploy files changed';
+              const t = team.toLowerCase();
+              if (t.includes('rust')) return 'Rust files changed';
+              if (t.includes('python')) return 'Python files changed';
+              if (t.includes('devops')) return 'CI/infra files changed';
+              if (t.includes('deploy')) return 'Deploy files changed';
               return 'Files changed';
             }
.github/workflows/lgtm-bot-stale.yml (1)

52-67: Potential GitHub API rate-limit pressure for repos with many open PRs.

This loop makes 1+ API calls per open non-draft PR (at minimum listRequestedReviewers, plus conditionally listComments and listEvents). With a GITHUB_TOKEN rate limit of 1,000 requests/hour for Actions, a repo with ~200+ open PRs could approach the limit in a single run.

Consider adding a batch limit (e.g., process only the first N PRs sorted by oldest update) or short-circuiting PRs that clearly don't need nudging before making additional API calls.

.github/scripts/lgtm_bot_diagnose.py (2)

464-475: is_fork parameter is unused in build_diagnostic_comment.

Static analysis (Ruff ARG001) correctly flags that is_fork on line 472 is never referenced in the function body. It's passed from main() at line 726 but has no effect.

Either remove the parameter or use it for fork-specific messaging (e.g., the CI trigger section currently uses the ci_not_triggered heuristic regardless of fork status).

Option A: Remove unused parameter
 def build_diagnostic_comment(
     pr: dict,
     ci_results: dict,
     failing_checks: list[str],
     approvals: list[str],
     changes_requested: list[str],
     unresolved: list[dict],
     team_files: dict[str, list[str]],
-    is_fork: bool,
     head_sha: str,
     llm_response: str | None,
 ) -> str:

And in main():

     comment_body = build_diagnostic_comment(
         pr,
         ci_results,
         failing_checks,
         approvals,
         changes_requested,
         unresolved,
         team_files,
-        is_fork,
         head_sha,
         llm_response,
     )

40-43: Missing env vars will crash with an unhelpful KeyError.

os.environ["GITHUB_TOKEN"] and int(os.environ["PR_NUMBER"]) will raise KeyError / ValueError with no guidance on what's missing. Consider wrapping in a startup check with a clear error message since this script is user-facing (invoked via /lgtm-bot diagnose).

Suggested improvement
-GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
-GITHUB_REPOSITORY = os.environ.get("GITHUB_REPOSITORY", "ai-dynamo/dynamo")
-PR_NUMBER = int(os.environ["PR_NUMBER"])
+GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
+if not GITHUB_TOKEN:
+    sys.exit("Error: GITHUB_TOKEN environment variable is required")
+GITHUB_REPOSITORY = os.environ.get("GITHUB_REPOSITORY", "ai-dynamo/dynamo")
+_pr_num = os.environ.get("PR_NUMBER")
+if not _pr_num:
+    sys.exit("Error: PR_NUMBER environment variable is required")
+try:
+    PR_NUMBER = int(_pr_num)
+except ValueError:
+    sys.exit(f"Error: PR_NUMBER must be an integer, got: {_pr_num}")

Comment thread .github/scripts/lgtm_bot_diagnose.py
Comment thread .github/workflows/lgtm-bot-pr-open.yml Outdated
Comment thread .github/workflows/lgtm-bot.yml
… comments

- Handle pending commit status separately from fail in both Python and
  JS (DCO check processing shows as "running" not "failing")
- Paginate comment search from newest first in updateChecklist and
  CODEOWNERS comment (prevents duplicates on PRs with 100+ comments)
- Brand all bot comments with "LGTM Bot" header for clear identity

Note: comments currently post as github-actions[bot]. A dedicated
GitHub App account (lgtm-bot[bot]) can be created in a future update
once the bot proves useful.

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/ok to test 8561dd1

- Update CONTRIBUTING.md step 7 to explain that /ok to test applies
  to all PRs, not just forks — org members with GPG-signed commits
  trigger CI automatically and can skip this step
- Add "CI Triggering" subsection with link to GitHub GPG docs
- Update bot comments (CODEOWNERS visibility, diagnostics) to mention
  the GPG signing exception
- Change "Fork CI status" to "CI trigger status" since it applies to
  all PRs

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/ok to test 8f36874

@dillon-cullinan dillon-cullinan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the purpose of this PR?

I read the CONTRIBUTING and I'm not understanding what this is giving us that the github UI doesn't already show.

@github-actions

github-actions Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

🤖 LGTM Bot — Merge Checklist

  • Pre-merge CI (running...)
  • Copyright headers (running...)
  • DCO sign-off
  • PR title format
  • Full CI triggered (maintainer must run: /ok to test d62a165)
  • At least 1 approval (no approvals yet)
  • No outstanding change requests (changes requested)
  • Branch protection satisfied (pending)

When all items are checked, the lgtm label will be added automatically.
Run /diagnose for detailed analysis of any blockers.


This checklist updates automatically when CI completes, reviews change, or new commits are pushed.

Consolidate /lgtm-bot diagnose and /classify-errors into a single
/diagnose slash command. Refactor error classification module, fix
bugs found during multi-perspective code review, and harden all
workflows for production use.

Key decision: A single /diagnose command replaces two overlapping
commands. The diagnose workflow already embeds the error classifier
output when CI is failing, making the standalone /classify-errors
redundant. /lgtm-bot diagnose remains as a backward-compatible alias.

Changes:

Unified /diagnose command:
- Trigger on /diagnose (primary) and /lgtm-bot diagnose (alias)
  using startsWith() to prevent false triggers from casual mentions
- Delete lgtm-bot-classify-errors.yml (redundant)
- Add concurrency group per PR to prevent duplicate runs

Reviewer assignment consolidation:
- Merge assign-reviewers job into pr-onboard (eliminates ~200 lines
  of duplicated CODEOWNERS/glob/file-fetching code)
- Support both CODEOWNERS team resolution and YAML roster
- Add example YAML format in code and docs

Error classification refactor:
- Rename anthropic_* config fields to api_key/model (NVIDIA is now
  the primary backend, not Anthropic)
- Deduplicate summary prompt and retry logic in LLM client
- Remove dead code: ClassificationResult, call_llm(), 6 unused
  config fields, redundant run_id computation
- Move classify_workflow_errors.py to .github/scripts/

Bug fixes (found via DevOps/Dev/Leadership review simulation):
- Fix /lgtm-bot diagnose alias (contains() didn't match substring)
- Restore config.max_error_length (removed but still referenced)
- Fix stdout contamination in OUTPUT_ONLY mode (bare print() in
  helper functions polluted markdown output for subprocess capture)
- Add threading.Lock to RateLimiter (used concurrently via
  ThreadPoolExecutor without synchronization)
- Fix is_mergeable to check changes_requested
- Fix fetch_failed_logs stderr, graphql error handling, silent
  CODEOWNERS exception swallowing
- Fix sys.exit(0) on classifier exception -> sys.exit(1)

Hardening:
- Pin all actions to SHA (supply chain security)
- Add timeout-minutes to all jobs (5-15 min vs 6-hour default)
- Add continue-on-error on classifier step (observability, not gate)
- Add 5xx retry with exponential backoff in LLM client
- Make NVIDIA_INFERENCE_API_KEY optional in reusable workflow
- Add NVIDIA_INFERENCE_MODEL as reusable workflow input

Contributor experience:
- Tier-specific CI trigger guidance (first-time, L1, L2+/org)
- Auto-update footer on bot comments
- Updated LGTM_BOT.md with all workflows, secrets, and vars
- Updated CONTRIBUTING.md with /diagnose as primary command

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia
dagil-nvidia requested a review from a team as a code owner February 14, 2026 06:16
@dagil-nvidia dagil-nvidia changed the title ci: add LGTM Bot for PR merge readiness automation ci: add LGTM Bot with merge readiness tracking and AI-powered CI diagnostics Feb 14, 2026
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

1 similar comment
@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In @.github/scripts/lgtm_bot_diagnose.py:
- Around line 352-372: The diagnose_reviews function currently calls
github_request once and can miss reviews because GitHub paginates results;
modify diagnose_reviews to page through the reviews endpoint (e.g., add page and
per_page params to github_request calls, using per_page=100 and incrementing
page until an empty page is returned) and accumulate all review objects into the
reviews list before the existing latest_by_user/approvals logic; keep using
OWNER and REPO in the endpoint string and reuse github_request to fetch each
page.
- Around line 82-93: The github_request function calls urllib.request.urlopen
without a timeout and can hang; match github_graphql by passing a timeout (e.g.,
timeout=30) to urllib.request.urlopen(req, timeout=30) or use a shared constant
for the timeout value, keeping the existing HTTPError handling intact (preserve
the error_body read/print/raise logic) so the request won't block indefinitely.

In @.github/workflows/lgtm-bot.yml:
- Around line 386-390: The call to github.rest.pulls.listReviews is not
paginated so PRs with >30 reviews can miss reviews; replace the single-page call
(const { data: reviews } = await github.rest.pulls.listReviews({...})) with a
paginated fetch using the GitHub client's paginate helper (e.g., use
github.paginate(github.rest.pulls.listReviews, { owner: context.repo.owner,
repo: context.repo.repo, pull_number: prNumber }) to collect all review pages
into a single reviews array) and then use that aggregated reviews array for
approval/state logic; apply the identical pagination change to the synchronize
path that also calls github.rest.pulls.listReviews.
- Around line 488-513: The curl invocation using SLACK_WEBHOOK_URL can leak the
secret in curl's error output; update the run step that calls curl (the command
referencing SLACK_WEBHOOK_URL and using -sSf) so stderr is suppressed or removed
from logs—e.g., remove -S or redirect curl's stderr to /dev/null (append
2>/dev/null) or otherwise avoid printing curl's error details; target the curl
command in the workflow where SLACK_WEBHOOK_URL, PR_NUMBER, PR_TITLE, PR_URL,
and PR_AUTHOR are used and ensure the secret URL is not emitted on failure.
- Around line 117-149: The issue: updateChecklist currently passes the
unsupported direction option and fetches comments oldest-first, making the
search inefficient. Fix it in updateChecklist by removing the direction
parameter and use octokit pagination to get all comments then search from
newest: call github.paginate(github.rest.issues.listComments, { owner:
context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page:
100 }) to retrieve all comments, then find the most recent comment containing
CHECKLIST_MARKER by iterating the returned array from the end (or using
.reverse().find) and assign it to existing before updating/creating the comment
as done now.

In `@error_classification/classifier.py`:
- Around line 198-211: The token fields (prompt_tokens, completion_tokens,
cached_tokens) are currently copied from result to every ErrorClassification in
classifier.py, inflating aggregated totals in classify_workflow_errors.py; fix
by computing the total tokens once from result (result.get("prompt_tokens",0),
etc.), dividing them evenly across the number of classifications you are
creating (num_errors = len(classifications) or similar), assign tokens_per =
total // num_errors for each, add any remainder to the first classification (or
the first few) so totals sum to the original, and then set
prompt_tokens/completion_tokens/cached_tokens on each ErrorClassification
accordingly (use the existing error_data/result variables and the
ErrorClassification construction block to apply the per-class values).

In `@error_classification/claude_client.py`:
- Around line 38-56: The wait_if_needed method in RateLimiter captures now
before possibly sleeping, then appends that stale timestamp to self.requests;
change the logic so that after any sleep you record the current time (recompute
now or use a new timestamp) and append that fresh timestamp to self.requests
(keeping the operation inside the same self._lock block) so the request reflects
the actual time it occurred and the rate window is correct.

In `@error_classification/config.py`:
- Around line 59-67: The MAX_RPM environment parsing currently uses
int(os.getenv("MAX_RPM", "50")) which raises an unhelpful ValueError if the env
var is non-numeric; change Config construction to use a safe parser (implement a
static/helper method named _parse_int_env(name: str, default: int) that reads
os.getenv(name), returns default on empty, and raises a clear ValueError like
"{name} must be an integer, got: {raw!r}" on invalid input) and call it instead
of int(...) when setting max_rpm in the cls(...) return.

In `@error_classification/pr_commentator.py`:
- Around line 319-340: The _find_pr_by_commit function returns the raw JSON
value for pulls[0].get("number") which may be None or non-int; update
_find_pr_by_commit to coerce and validate the PR number before returning:
extract the value from pulls[0].get("number"), check it's not None and is an
int-like value (e.g., str of digits or int), convert with int(...) and return
that, otherwise log/ignore and return None; ensure the function signature
Optional[int] is honored and keep the existing exception handling in
_find_pr_by_commit.
- Around line 170-207: The requests.patch and requests.post calls that
post/update GitHub comments (using existing_comment_id, pr_number in
pr_commentator) lack a timeout and can hang; add a timeout parameter (e.g.,
timeout=10) to both requests.patch(...) and requests.post(...) calls, and also
ensure the helper _find_existing_comment (referenced elsewhere in this module)
is updated to pass timeout to any requests it makes so all HTTP calls in this
class use a consistent timeout value.
🧹 Nitpick comments (16)
.github/workflows/lgtm-bot-stale.yml (1)

97-103: The cooldown check only scans the last 10 comments — could miss an old nudge in busy PRs.

If a PR receives many non-bot comments (>10) within the 24h cooldown window, the bot won't find its previous nudge and may post a duplicate. This is unlikely in practice but worth noting.

A safer approach is to filter by since (ISO timestamp for 24h ago) instead of relying on a small per_page:

💡 Suggested improvement
+                const sinceDate = new Date(now - NUDGE_COOLDOWN_HOURS * MS_PER_HOUR).toISOString();
                 const { data: comments } = await github.rest.issues.listComments({
                   owner: context.repo.owner,
                   repo: context.repo.repo,
                   issue_number: prNumber,
-                  per_page: 10,
-                  direction: 'desc',
+                  since: sinceDate,
+                  per_page: 100,
                 });
CONTRIBUTING.md (1)

239-245: Minor: Lines 243–245 describe overlapping functionality — consider consolidating.

"Automatic", "On-demand", and "Via diagnose" bullets under Error Classification have redundancy — the last two both describe /diagnose triggering classification.

Suggested simplification
 - **Automatic** – The error classifier runs as the final step in CI pipelines (`pre-merge`, `pr`, `ci-test-suite`, `container-validation-dynamo`, `copyright-checks`, `build-frontend-image`). When failures are detected, it posts a PR comment categorizing each as **infrastructure error** (network/runner issues) or **code error** (build/test/compilation failures).
-- **On-demand** – Comment `/diagnose` on any PR to run classification along with the full merge-readiness diagnostic.
-- **Via diagnose** – The `/diagnose` command automatically runs the error classifier when CI failures are present.
+- **On-demand** – Comment `/diagnose` on any PR to run classification along with the full merge-readiness diagnostic. The error classifier is automatically included when CI failures are present.
.github/workflows/classify-workflow-errors-reusable.yml (1)

47-52: pip install runs outside the venv if the venv creation fails silently.

The sudo apt-get ... || true on line 49 swallows errors. If python3-venv installation fails, the python3 -m venv on line 50 will also fail, but the pip install on line 52 would then install into the system Python (or fail outright). Since this step doesn't use set -e, a failed venv creation won't halt the step. Consider adding set -e or removing || true to fail fast.

Suggested fix
       - name: Install dependencies
         run: |
-          sudo apt-get update && sudo apt-get install -y python3-venv || true
+          sudo apt-get update && sudo apt-get install -y python3-venv
           python3 -m venv /tmp/error-classification-venv
           . /tmp/error-classification-venv/bin/activate
           pip install -r error_classification/requirements.txt
.github/workflows/lgtm-bot-diagnose.yml (1)

67-72: Same || true silencing pattern as the reusable workflow — venv failures may go unnoticed.

Same issue as in classify-workflow-errors-reusable.yml: || true on line 69 swallows apt-get failures, risking a broken venv and confusing downstream errors.

.github/workflows/ci-test-suite.yml (1)

1001-1010: enable_pr_comments defaults to 'true' but this workflow runs in nightly/post-merge contexts without a PR.

This workflow is called by nightly-ci.yml and post-merge-ci.yml (per the header comment). In those contexts, there's no PR to comment on. While continue-on-error: true in the reusable workflow prevents failures, you could explicitly pass enable_pr_comments: 'false' to avoid unnecessary API calls and log noise.

Suggested fix
   classify-errors:
     if: always() && !cancelled()
     needs: [build-amd64, build-arm64, build-cuda13-amd64, build-cuda13-arm64, unit-tests, integration-tests, e2e-single-gpu-tests, e2e-multi-gpu-tests, fault-tolerance-tests]
     permissions:
       contents: read
       checks: write
       pull-requests: write
     uses: ./.github/workflows/classify-workflow-errors-reusable.yml
+    with:
+      enable_pr_comments: 'false'
     secrets: inherit
.github/LGTM_BOT.md (1)

82-88: LGTM_BOT_REVIEWERS_PER_TEAM is documented in two separate tables.

Lines 86 and 140 both document the same variable. Consider removing one to avoid drift if the default or description changes.

Also applies to: 136-141

.github/workflows/lgtm-bot-pr-open.yml (1)

310-316: teamReason uses hardcoded substring checks — fragile for evolving team names.

The function checks for substrings like 'rust', 'python', 'Devops', 'deploy' in team names. Note that 'Devops' is case-sensitive (capital D), so a team named devops or DevOps wouldn't match. This is minor but could produce less informative comments if team naming conventions change.

error_classification/__init__.py (1)

14-31: __all__ is not sorted (Ruff RUF022).

Static analysis flags that __all__ is not isort-style sorted. Consider sorting for consistency.

Proposed fix
 __all__ = [
-    # Config
-    "Config",
-    "get_config",
-    "ERROR_CATEGORIES",
-    # Classifier
-    "ErrorClassifier",
-    "ErrorClassification",
-    # Error context
-    "ErrorContext",
-    # Claude client
+    "CATEGORY_SEVERITY",
     "ClaudeClient",
-    # Prompts
-    "get_category_definitions",
-    # PR comments
-    "PRCommentator",
-    "CATEGORY_SEVERITY",
+    "Config",
+    "ERROR_CATEGORIES",
+    "ErrorClassification",
+    "ErrorClassifier",
+    "ErrorContext",
+    "PRCommentator",
+    "get_category_definitions",
+    "get_config",
 ]
error_classification/config.py (1)

69-76: validate() is effectively unreachable for api_key when using from_env.

from_env already raises ValueError if api_key is missing (Line 48-51), so validate() checking api_key again (Line 73) will never fire for instances created via from_envget_config. This isn't a bug (it guards direct construction), but validate() could be extended to also check api_format is one of {"openai", "anthropic"} — an invalid api_format silently falls through to the Anthropic path in ClaudeClient.__init__.

.github/scripts/classify_workflow_errors.py (3)

142-172: github_token and repo parameters are unused.

fetch_job_steps only reads job.get("steps") and never uses github_token or repo. This creates a misleading interface suggesting an API call is made.

Proposed fix
-def fetch_job_steps(
-    job: Dict[str, Any], github_token: str, repo: str
-) -> List[Dict[str, Any]]:
+def fetch_job_steps(job: Dict[str, Any]) -> List[Dict[str, Any]]:

Update any call sites accordingly (though this function doesn't appear to be called in this file).


257-272: sys.stdout reassignment is a global side-effect affecting all imported modules.

In OUTPUT_ONLY mode, sys.stdout = sys.stderr (Line 265) means every print() in third-party code (including PRCommentator, ErrorClassifier, etc.) also writes to stderr. This is intentional, but if any called code caches a reference to sys.stdout before this assignment (e.g., PRCommentator.__init__), the output routing breaks. The PRCommentator class uses print() directly so it's fine, but this is fragile coupling.


214-248: Fallback returns the entire job log when step markers aren't found.

Line 248 returns full_log unfiltered, which for large jobs could be hundreds of thousands of characters. While claude_client.py truncates to MAX_LOG_LENGTH, the fallback still means the caller receives far more data than expected from a "step log extraction" function.

Consider adding a size cap in the fallback:

Proposed fix
     # Last resort: return full log (filtered later)
-    return full_log
+    # Cap to avoid passing excessively large text when markers aren't found
+    return full_log[-50000:] if len(full_log) > 50000 else full_log
error_classification/pr_commentator.py (1)

342-411: error_contexts parameter is unused in _build_summary_markdown_body.

The parameter is declared (Line 343) but never referenced in the method body. Ruff ARG002 flags this. Either remove it or use it to enrich the markdown output.

error_classification/claude_client.py (3)

116-123: Log truncation logic is duplicated between analyze_full_job_log and _analyze_full_log_openai_format.

Lines 117-123 and Lines 362-367 contain identical truncation logic. Extract to a shared helper to keep the truncation strategy consistent and DRY.

Proposed fix
+    `@staticmethod`
+    def _truncate_log(job_log: str) -> str:
+        """Truncate log to MAX_LOG_LENGTH, keeping the tail (most relevant for failures)."""
+        if len(job_log) > MAX_LOG_LENGTH:
+            truncated_length = len(job_log) - MAX_LOG_LENGTH
+            return (
+                f"[... truncated first {truncated_length} chars ...]\n\n"
+                + job_log[-MAX_LOG_LENGTH:]
+            )
+        return job_log

Then replace both occurrences:

-        if len(job_log) > MAX_LOG_LENGTH:
-            truncated_length = len(job_log) - MAX_LOG_LENGTH
-            job_log = (
-                f"[... truncated first {truncated_length} chars ...]\n\n"
-                + job_log[-MAX_LOG_LENGTH:]
-            )
+        job_log = self._truncate_log(job_log)

Also applies to: 361-367


250-254: Re-raised exceptions should chain with from to preserve the original traceback.

Lines 251 and 254 raise new ValueError exceptions from within except blocks without chaining. This loses the original traceback context.

Proposed fix
         except json.JSONDecodeError as e:
-            raise ValueError(f"Invalid JSON in response: {e}")
+            raise ValueError(f"Invalid JSON in response: {e}") from e
 
         except Exception as e:
-            raise ValueError(f"Error parsing response: {e}")
+            raise ValueError(f"Error parsing response: {e}") from e

204-248: JSON extraction via first { / last } is fragile if the LLM response contains markdown code fences with nested JSON.

response_text.find("{") and rfind("}") will work for simple responses, but if the LLM wraps the JSON in a markdown code block (e.g., ```json { ... } ``` followed by explanatory text containing }), rfind may capture beyond the intended JSON. Since LLM output is unpredictable, consider first stripping markdown code fences before the find/rfind approach.

Comment on lines +82 to +93
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = ""
try:
error_body = e.read().decode()
except Exception:
pass
print(f"GitHub API error: {e.code} {e.reason} {error_body}", file=sys.stderr)
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing timeout on REST API calls — could hang indefinitely.

github_request uses urlopen without a timeout, unlike github_graphql which sets timeout=30. In CI, a hung network call would block the workflow until the runner's global timeout kills it.

Suggested fix
-        with urllib.request.urlopen(req) as resp:
+        with urllib.request.urlopen(req, timeout=30) as resp:
🧰 Tools
🪛 Ruff (0.15.0)

[error] 82-82: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 84-84: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 90-91: try-except-pass detected, consider logging the exception

(S110)


[warning] 90-90: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In @.github/scripts/lgtm_bot_diagnose.py around lines 82 - 93, The
github_request function calls urllib.request.urlopen without a timeout and can
hang; match github_graphql by passing a timeout (e.g., timeout=30) to
urllib.request.urlopen(req, timeout=30) or use a shared constant for the timeout
value, keeping the existing HTTPError handling intact (preserve the error_body
read/print/raise logic) so the request won't block indefinitely.

Comment on lines +352 to +372
def diagnose_reviews(pr_number: int, pr_author: str) -> tuple[list[str], list[str]]:
"""Check review status."""
reviews = github_request(f"/repos/{OWNER}/{REPO}/pulls/{pr_number}/reviews")

latest_by_user: dict[str, dict] = {}
for review in reviews:
user = review["user"]["login"]
if user == pr_author:
continue
if review["state"] not in ("APPROVED", "CHANGES_REQUESTED"):
continue
existing = latest_by_user.get(user)
if not existing or review["submitted_at"] > existing["submitted_at"]:
latest_by_user[user] = review

approvals = [u for u, r in latest_by_user.items() if r["state"] == "APPROVED"]
changes_requested = [
u for u, r in latest_by_user.items() if r["state"] == "CHANGES_REQUESTED"
]

return approvals, changes_requested

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

diagnose_reviews doesn't paginate — may miss reviews on busy PRs.

The GitHub API defaults to 30 results per page. PRs with many review iterations (common on large PRs) could have >30 review entries, causing the function to miss some approvals or change requests.

Suggested fix
 def diagnose_reviews(pr_number: int, pr_author: str) -> tuple[list[str], list[str]]:
     """Check review status."""
-    reviews = github_request(f"/repos/{OWNER}/{REPO}/pulls/{pr_number}/reviews")
+    reviews: list[dict] = []
+    page = 1
+    while True:
+        batch = github_request(
+            f"/repos/{OWNER}/{REPO}/pulls/{pr_number}/reviews"
+            f"?per_page=100&page={page}"
+        )
+        reviews.extend(batch)
+        if len(batch) < 100:
+            break
+        page += 1
🤖 Prompt for AI Agents
In @.github/scripts/lgtm_bot_diagnose.py around lines 352 - 372, The
diagnose_reviews function currently calls github_request once and can miss
reviews because GitHub paginates results; modify diagnose_reviews to page
through the reviews endpoint (e.g., add page and per_page params to
github_request calls, using per_page=100 and incrementing page until an empty
page is returned) and accumulate all review objects into the reviews list before
the existing latest_by_user/approvals logic; keep using OWNER and REPO in the
endpoint string and reuse github_request to fetch each page.

Comment on lines +117 to +149
async function updateChecklist(prNumber, body) {
// Search from newest first — checklist comment is frequently updated
let existing = null;
let page = 1;
while (!existing) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
page,
direction: 'desc',
});
if (comments.length === 0) break;
existing = comments.find(c => c.body && c.body.includes(CHECKLIST_MARKER));
page++;
}
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

GitHub REST API list issue comments direction parameter supported

💡 Result:

For List issue comments on a specific issue:

  • Endpoint: GET /repos/{owner}/{repo}/issues/{issue_number}/comments
  • direction is not supported (there’s no sort/direction query params for this endpoint). Comments are returned ordered by ascending ID; available query params are since, per_page, and page. [1]

If you need direction, that’s supported only on the repository-wide listing endpoint:

  • Endpoint: GET /repos/{owner}/{repo}/issues/comments
  • Supports sort (created or updated) and direction (asc/desc), but direction is ignored unless sort is provided. [1]

Sources:
[1] GitHub Docs – REST API endpoints for issue comments (parameters for both endpoints) (docs.github.com)

Citations:


direction: 'desc' is not supported by the issue comments endpoint — comments will be fetched oldest-first instead.

The GitHub REST API for listing issue comments does not support a direction parameter on the issue-specific endpoint. The parameter is silently ignored and comments are returned in ascending order by ID. This contradicts the intent of Line 118's comment about searching "from newest first."

The pagination loop will still work correctly, but on active PRs it becomes inefficient by iterating through all comments from oldest to newest.

🤖 Prompt for AI Agents
In @.github/workflows/lgtm-bot.yml around lines 117 - 149, The issue:
updateChecklist currently passes the unsupported direction option and fetches
comments oldest-first, making the search inefficient. Fix it in updateChecklist
by removing the direction parameter and use octokit pagination to get all
comments then search from newest: call
github.paginate(github.rest.issues.listComments, { owner: context.repo.owner,
repo: context.repo.repo, issue_number: prNumber, per_page: 100 }) to retrieve
all comments, then find the most recent comment containing CHECKLIST_MARKER by
iterating the returned array from the end (or using .reverse().find) and assign
it to existing before updating/creating the comment as done now.

Comment thread .github/workflows/lgtm-bot.yml Outdated
Comment on lines +386 to +390
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reviews are not paginated — PRs with 30+ reviews may lose data.

github.rest.pulls.listReviews defaults to 30 items per page. For high-activity PRs, later reviews (including the most recent approvals or change-requests) may be missed, causing the bot to show stale approval/changes-requested state or to apply/remove the label incorrectly. The same issue applies to the synchronize path at Lines 277-281.

Proposed fix — paginate reviews
-            const { data: reviews } = await github.rest.pulls.listReviews({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              pull_number: prNumber,
-            });
+            let reviews = [];
+            let reviewPage = 1;
+            while (true) {
+              const { data: batch } = await github.rest.pulls.listReviews({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                pull_number: prNumber,
+                per_page: 100,
+                page: reviewPage,
+              });
+              reviews = reviews.concat(batch);
+              if (batch.length < 100) break;
+              reviewPage++;
+            }

Apply the same pattern to the synchronize path (Lines 277-281).

📝 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
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
let reviews = [];
let reviewPage = 1;
while (true) {
const { data: batch } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
page: reviewPage,
});
reviews = reviews.concat(batch);
if (batch.length < 100) break;
reviewPage++;
}
🤖 Prompt for AI Agents
In @.github/workflows/lgtm-bot.yml around lines 386 - 390, The call to
github.rest.pulls.listReviews is not paginated so PRs with >30 reviews can miss
reviews; replace the single-page call (const { data: reviews } = await
github.rest.pulls.listReviews({...})) with a paginated fetch using the GitHub
client's paginate helper (e.g., use
github.paginate(github.rest.pulls.listReviews, { owner: context.repo.owner,
repo: context.repo.repo, pull_number: prNumber }) to collect all review pages
into a single reviews array) and then use that aggregated reviews array for
approval/state logic; apply the identical pagination change to the synchronize
path that also calls github.rest.pulls.listReviews.

Comment on lines +488 to +513
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_LGTM_WEBHOOK_URL }}
PR_NUMBER: ${{ steps.lgtm.outputs.pr_number }}
PR_TITLE: ${{ steps.lgtm.outputs.pr_title }}
PR_URL: ${{ steps.lgtm.outputs.pr_url }}
PR_AUTHOR: ${{ steps.lgtm.outputs.pr_author }}
run: |
curl -sSf -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg pr_num "$PR_NUMBER" \
--arg pr_title "$PR_TITLE" \
--arg pr_url "$PR_URL" \
--arg pr_author "$PR_AUTHOR" \
'{
text: ":white_check_mark: PR #\($pr_num) is ready to merge",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ":white_check_mark: *PR Ready to Merge*\n<\($pr_url)|#\($pr_num): \($pr_title)>\nAuthor: \($pr_author) | All CI checks passing, approved, DCO signed"
}
}
]
}')"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Slack webhook URL is passed through environment and logged shell context — ensure curl errors don't leak the URL.

curl -sSf will print the URL in its error message if the request fails. Since SLACK_WEBHOOK_URL is in the URL argument, a failed curl could leak the secret webhook URL into workflow logs.

Proposed fix — suppress URL from error output
-          curl -sSf -X POST "$SLACK_WEBHOOK_URL" \
+          curl -sSf --fail-with-body -X POST "$SLACK_WEBHOOK_URL" \
+            --no-progress-meter \
             -H 'Content-Type: application/json' \
-            -d "$(jq -n \
+            -d "$(jq -n \

Alternatively, redirect stderr to suppress the URL:

-          curl -sSf -X POST "$SLACK_WEBHOOK_URL" \
+          curl -sS -X POST "$SLACK_WEBHOOK_URL" \
             -H 'Content-Type: application/json' \
-            -d "$(jq -n \
+            -d "$(jq -n \
              ...
-              }')"
+              }')" 2>&1 | grep -v "$SLACK_WEBHOOK_URL" || true
🤖 Prompt for AI Agents
In @.github/workflows/lgtm-bot.yml around lines 488 - 513, The curl invocation
using SLACK_WEBHOOK_URL can leak the secret in curl's error output; update the
run step that calls curl (the command referencing SLACK_WEBHOOK_URL and using
-sSf) so stderr is suppressed or removed from logs—e.g., remove -S or redirect
curl's stderr to /dev/null (append 2>/dev/null) or otherwise avoid printing
curl's error details; target the curl command in the workflow where
SLACK_WEBHOOK_URL, PR_NUMBER, PR_TITLE, PR_URL, and PR_AUTHOR are used and
ensure the secret URL is not emitted on failure.

Comment on lines +198 to +211
primary_category=error_data["primary_category"],
confidence_score=error_data["confidence_score"],
root_cause_summary=error_data["root_cause_summary"],
classification_method="full_log_analysis",
model_version=result.get("model_version"),
classified_at=datetime.now(timezone.utc).isoformat(),
is_duplicate=False,
occurrence_count=1,
first_seen=datetime.now(timezone.utc).isoformat(),
last_seen=datetime.now(timezone.utc).isoformat(),
prompt_tokens=result.get("prompt_tokens", 0),
completion_tokens=result.get("completion_tokens", 0),
cached_tokens=result.get("cached_tokens", 0),
timestamp=datetime.now(timezone.utc).isoformat(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Token usage is duplicated across all classifications from the same API call — aggregate totals will be inflated.

Lines 208-210 assign the total prompt_tokens, completion_tokens, and cached_tokens from the single LLM API call to every ErrorClassification produced from that call. When classify_workflow_errors.py (Lines 506-508) sums c.prompt_tokens across all classifications, the reported totals are multiplied by the number of errors found per job.

For example, one API call costing 5000 prompt tokens that finds 3 errors will report 15000 prompt tokens total.

Proposed fix — distribute tokens across errors or store only on the first
+        num_errors = len(result.get("errors_found", []))
         # Convert results to ErrorClassification objects
         classifications = []
-        for error_data in result.get("errors_found", []):
+        for i, error_data in enumerate(result.get("errors_found", [])):
             error_text = error_data.get("log_excerpt", "")
 
             classification = ErrorClassification(
                 ...
-                prompt_tokens=result.get("prompt_tokens", 0),
-                completion_tokens=result.get("completion_tokens", 0),
-                cached_tokens=result.get("cached_tokens", 0),
+                # Attribute token usage only to the first error to avoid double-counting
+                prompt_tokens=result.get("prompt_tokens", 0) if i == 0 else 0,
+                completion_tokens=result.get("completion_tokens", 0) if i == 0 else 0,
+                cached_tokens=result.get("cached_tokens", 0) if i == 0 else 0,
                 ...
             )
🤖 Prompt for AI Agents
In `@error_classification/classifier.py` around lines 198 - 211, The token fields
(prompt_tokens, completion_tokens, cached_tokens) are currently copied from
result to every ErrorClassification in classifier.py, inflating aggregated
totals in classify_workflow_errors.py; fix by computing the total tokens once
from result (result.get("prompt_tokens",0), etc.), dividing them evenly across
the number of classifications you are creating (num_errors =
len(classifications) or similar), assign tokens_per = total // num_errors for
each, add any remainder to the first classification (or the first few) so totals
sum to the original, and then set prompt_tokens/completion_tokens/cached_tokens
on each ErrorClassification accordingly (use the existing error_data/result
variables and the ErrorClassification construction block to apply the per-class
values).

Comment thread error_classification/claude_client.py Outdated
Comment on lines +38 to +56
def wait_if_needed(self):
"""Wait if rate limit would be exceeded."""
with self._lock:
now = time.time()

# Remove requests older than 1 minute
self.requests = [
req_time for req_time in self.requests if now - req_time < 60
]

# If at limit, wait
if len(self.requests) >= self.max_rpm:
oldest_request = self.requests[0]
wait_time = 60 - (now - oldest_request)
if wait_time > 0:
print(f"⏳ Rate limit reached, waiting {wait_time:.1f}s")
time.sleep(wait_time)

self.requests.append(now)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

RateLimiter.wait_if_needed records a stale timestamp after sleeping.

now is captured at Line 41 before the potential sleep at Line 54. After sleeping (potentially tens of seconds), Line 56 appends the pre-sleep now value. This makes the request appear older than it actually is, causing subsequent calls to expire it from the window earlier and potentially allowing the rate limit to be slightly exceeded.

Proposed fix
             if len(self.requests) >= self.max_rpm:
                 oldest_request = self.requests[0]
                 wait_time = 60 - (now - oldest_request)
                 if wait_time > 0:
                     print(f"⏳ Rate limit reached, waiting {wait_time:.1f}s")
                     time.sleep(wait_time)
+                    # Re-capture time after sleep
+                    now = time.time()
+                    # Clean up again after sleep
+                    self.requests = [
+                        req_time for req_time in self.requests if now - req_time < 60
+                    ]
 
             self.requests.append(now)
🤖 Prompt for AI Agents
In `@error_classification/claude_client.py` around lines 38 - 56, The
wait_if_needed method in RateLimiter captures now before possibly sleeping, then
appends that stale timestamp to self.requests; change the logic so that after
any sleep you record the current time (recompute now or use a new timestamp) and
append that fresh timestamp to self.requests (keeping the operation inside the
same self._lock block) so the request reflects the actual time it occurred and
the rate window is correct.

Comment on lines +59 to +67
return cls(
api_key=api_key,
model=model,
api_format=os.getenv("API_FORMAT", "openai"), # default openai for NVIDIA
api_base_url=os.getenv(
"API_BASE_URL", "https://inference-api.nvidia.com/v1"
), # default NVIDIA
max_rpm=int(os.getenv("MAX_RPM", "50")),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

int(os.getenv("MAX_RPM", "50")) will crash with an unhelpful traceback if the env var is non-numeric.

If someone sets MAX_RPM=fast, this raises a bare ValueError from int(). A small guard or a friendlier message would help operators debug misconfiguration.

Proposed fix
-            max_rpm=int(os.getenv("MAX_RPM", "50")),
+            max_rpm=cls._parse_int_env("MAX_RPM", 50),

Add a helper:

`@staticmethod`
def _parse_int_env(name: str, default: int) -> int:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        return int(raw)
    except ValueError:
        raise ValueError(f"{name} must be an integer, got: {raw!r}")
🤖 Prompt for AI Agents
In `@error_classification/config.py` around lines 59 - 67, The MAX_RPM environment
parsing currently uses int(os.getenv("MAX_RPM", "50")) which raises an unhelpful
ValueError if the env var is non-numeric; change Config construction to use a
safe parser (implement a static/helper method named _parse_int_env(name: str,
default: int) that reads os.getenv(name), returns default on empty, and raises a
clear ValueError like "{name} must be an integer, got: {raw!r}" on invalid
input) and call it instead of int(...) when setting max_rpm in the cls(...)
return.

Comment on lines +170 to +207
# Post or update comment via GitHub API
try:
if existing_comment_id:
# Update existing comment
url = f"https://api.github.com/repos/{self.repo}/issues/comments/{existing_comment_id}"
response = requests.patch(
url, headers=self.headers, json={"body": markdown}
)

if response.status_code == 200:
print(
f"✅ Updated existing CI-errors comment {existing_comment_id} on PR #{pr_number}"
)
return True
else:
print(f"⚠️ Failed to update comment: HTTP {response.status_code}")
print(f" Response: {response.text[:200]}")
return False
else:
# Create new comment
url = f"https://api.github.com/repos/{self.repo}/issues/{pr_number}/comments"
response = requests.post(
url, headers=self.headers, json={"body": markdown}
)

if response.status_code == 201:
print(f"✅ Created PR comment on PR #{pr_number}")
return True
else:
print(
f"⚠️ Failed to create PR comment: HTTP {response.status_code}"
)
print(f" Response: {response.text[:200]}")
return False

except Exception as e:
print(f"⚠️ Error creating PR comment: {e}")
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing timeout on requests.patch and requests.post calls — can hang indefinitely.

Lines 175 and 191 make HTTP requests without a timeout parameter. If the GitHub API stalls, the workflow step will hang until the 5-minute job timeout. Other request calls in this PR (e.g., _find_pr_by_commit at Line 324) correctly set timeout=10. Static analysis (Ruff S113) flags these as well.

Proposed fix
                 response = requests.patch(
-                    url, headers=self.headers, json={"body": markdown}
+                    url, headers=self.headers, json={"body": markdown}, timeout=30
                 )
                 response = requests.post(
-                    url, headers=self.headers, json={"body": markdown}
+                    url, headers=self.headers, json={"body": markdown}, timeout=30
                 )

Also add timeout to _find_existing_comment at Line 221:

-                response = requests.get(url, headers=self.headers)
+                response = requests.get(url, headers=self.headers, timeout=10)
🧰 Tools
🪛 Ruff (0.15.0)

[error] 175-175: Probable use of requests call without timeout

(S113)


[error] 191-191: Probable use of requests call without timeout

(S113)


[warning] 205-205: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@error_classification/pr_commentator.py` around lines 170 - 207, The
requests.patch and requests.post calls that post/update GitHub comments (using
existing_comment_id, pr_number in pr_commentator) lack a timeout and can hang;
add a timeout parameter (e.g., timeout=10) to both requests.patch(...) and
requests.post(...) calls, and also ensure the helper _find_existing_comment
(referenced elsewhere in this module) is updated to pass timeout to any requests
it makes so all HTTP calls in this class use a consistent timeout value.

Comment on lines +319 to +340
def _find_pr_by_commit(self, commit_sha: str) -> Optional[int]:
"""Find PR number associated with a commit using GitHub API."""
try:
url = f"https://api.github.com/repos/{self.repo}/commits/{commit_sha}/pulls"
print(f" API request: {url}")
response = requests.get(url, headers=self.headers, timeout=10)
print(f" API response status: {response.status_code}")

if response.status_code == 200:
pulls = response.json()
print(f" API returned {len(pulls)} PR(s)")
if pulls and len(pulls) > 0:
pr_num = pulls[0].get("number")
print(f" First PR: #{pr_num}")
return pr_num
else:
print(f" API error: {response.text[:200]}")
except Exception as e:
print(f"⚠️ Error finding PR by commit: {e}")
traceback.print_exc()

return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

_find_pr_by_commit returns raw value from JSON without type coercion — may return None.

Line 331 uses pulls[0].get("number") which can return None if the number key is absent. The caller at Line 306 (pr_num = self._find_pr_by_commit(head_sha)) then checks if pr_num: which correctly gates a None, but the return type is Optional[int] while the actual return could be a non-int JSON value. Compare with Line 298 which wraps in int().

Proposed fix
                 if pulls and len(pulls) > 0:
                     pr_num = pulls[0].get("number")
-                    print(f"    First PR: #{pr_num}")
-                    return pr_num
+                    if pr_num is not None:
+                        print(f"    First PR: #{pr_num}")
+                        return int(pr_num)
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 336-336: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@error_classification/pr_commentator.py` around lines 319 - 340, The
_find_pr_by_commit function returns the raw JSON value for
pulls[0].get("number") which may be None or non-int; update _find_pr_by_commit
to coerce and validate the PR number before returning: extract the value from
pulls[0].get("number"), check it's not None and is an int-like value (e.g., str
of digits or int), convert with int(...) and return that, otherwise log/ignore
and return None; ensure the function signature Optional[int] is honored and keep
the existing exception handling in _find_pr_by_commit.

# --- GitHub API helpers ---


def github_request(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just use github CLI for all of these API requests? All of these scripts are completely overcomplicated.

headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we specifying the API version? Why not use default so that we get updates if they happen?



def diagnose_ci(head_sha: str) -> tuple[dict, list[str]]:
"""Check CI status for all required checks."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this better than a user just scrolling down and looking at the status checks?



def fetch_failed_logs(check_run_id: int) -> str:
"""Fetch annotations for a failed check run."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this better than a user just looking at any given workflows errors? Annotations aren't 100% accurate.

Comment thread .github/scripts/lgtm_bot_diagnose.py Outdated
def call_llm(failing_checks: list[str], logs: str) -> str | None:
"""Call NVIDIA Inference API for CI failure diagnosis."""
if not NVIDIA_API_KEY:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we returning None of the API key is absent? Why wouldn't this be an error?

id: lgtm
uses: actions/github-script@v7
with:
script: |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These scripts should all be outside the workflow. Defining functions inside workflows is bad practice and reduces re-usability.

}
}

function buildChecklist(checks, reviewStatus, bpState, headSha, ciTriggered) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Once again this information can already be gleaned from the github UI itself, there is no reason to reinvent the wheel for everything that Github already provides.

pull_request_review:
types: [submitted, dismissed]
# Remove label when new commits are pushed (uses _target for fork write access)
pull_request_target:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a security vulnerability. Don't use pull_request_target.

Comment on lines +198 to +222
if (context.eventName === 'workflow_run') {
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
prNumber = prs[0].number;
} else {
// Fallback for fork PRs where pull_requests array may be empty
const headBranch = context.payload.workflow_run.head_branch;
const headRepoOwner = context.payload.workflow_run.head_repository?.owner?.login;
if (headRepoOwner && headBranch) {
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${headRepoOwner}:${headBranch}`,
per_page: 1,
});
if (pulls.length > 0) {
prNumber = pulls[0].number;
}
}
}
} else {
// pull_request_target and pull_request_review both include PR
prNumber = context.payload.pull_request?.number;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So we are determining the pull request number with 30 lines of complicated code instead of just using {{ github.event.pull_request.number }}?

return;
}

// --- Check required CI statuses ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

More redundant code with the python script.

@dillon-cullinan

Copy link
Copy Markdown
Contributor

/lgtm-bot diagnose

@dillon-cullinan

dillon-cullinan commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

I just spent 30 minutes reviewing over 1000 lines of code and now the PR is 4000 lines of code?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In @.github/LGTM_BOT.md:
- Around line 155-162: The permissions doc currently lists "checks: read" but
the workflows' classify-errors job requires "checks: write"; update the
permissions example in .github/LGTM_BOT.md to use checks: write to match actual
workflows. Locate the permissions block and change checks from read to write,
and ensure the comment still explains that write is needed for the
classify-errors job (referencing the classify-errors job name used in the
workflows).
- Around line 136-141: Remove the duplicated "### Optional Variables" table
block that repeats the LGTM_BOT_REVIEWERS_PER_TEAM entry; specifically, delete
the second occurrence of the heading "### Optional Variables" and its table row
that lists `LGTM_BOT_REVIEWERS_PER_TEAM` so only the original documentation of
that variable remains (keep the first table where the variable is defined).

In @.github/scripts/classify_workflow_errors.py:
- Around line 505-508: The totals are inflated because the list classifications
contains multiple ErrorClassification entries per single API call; change the
aggregation to sum tokens once per unique API call instead of per
classification. Build a map keyed by a stable identifier on ErrorClassification
(e.g., an api_call_id or request_id attribute; if not available, use a fallback
unique tuple like (prompt_text, completion_text, timestamp)) and then compute
total_prompt_tokens, total_completion_tokens, and total_cached_tokens by summing
the values from each unique key rather than summing across classifications
directly; update the code around the variables total_prompt_tokens,
total_completion_tokens, total_cached_tokens to use this deduplicated
collection.
- Around line 96-113: The code may call find_latest_failed_pr_run with
github_token == None causing "Authorization: Bearer None" requests; before
invoking find_latest_failed_pr_run (the block using github_event_name and
run_id), ensure github_token is present or return early — e.g., check
github_token (and repo) alongside run_id or move the existing all([github_token,
repo, run_id]) guard above the workflow_dispatch branch; alternatively, add a
defensive check inside find_latest_failed_pr_run to bail when github_token is
falsy before building the Authorization header.

In @.github/workflows/lgtm-bot-stale.yml:
- Around line 96-108: The comments listing currently uses an unsupported
direction parameter and requests the 10 oldest comments, causing the
recent-nudge dedup (recentNudge using NUDGE_MARKER, per_page, MS_PER_HOUR,
NUDGE_COOLDOWN_HOURS) to miss recent nudges on busy PRs; update the logic to
fetch a larger page (e.g., per_page: 100) and search from the end (or take the
last N entries) to find NUDGE_MARKER within the cooldown window, and apply the
same change to the merge reminder dedup check (the equivalent
recentMergeReminder / MERGE_REMINDER_MARKER logic) so you reliably examine the
most recent comments instead of the oldest.

In `@error_classification/claude_client.py`:
- Around line 72-80: The current constructor branch that handles
config.api_format == "openai" silently defaults self.api_base_url to
"https://api.openai.com/v1" which can send the wrong API key; instead, update
the initialization in claude_client.py so that when config.api_format ==
"openai" and config.api_base_url is None you raise a clear exception (e.g.,
ValueError) telling the caller to provide api_base_url (or explicitly choose the
correct NVIDIA default) rather than silently falling back; change the code
around the block that sets self.client and self.api_base_url (the if
config.api_format == "openai" branch) to validate config.api_base_url and raise
an error with a descriptive message if it's missing.
- Around line 383-399: The code currently assumes the OpenAI response has
choices[0].message.content and will throw KeyError/IndexError; wrap the
extraction of content (after calling _openai_request_with_retry and parsing
response.json()) in a defensive check or try/except: verify data is a dict, that
"choices" exists and is a non-empty list and that choices[0] has "message" and
"content" keys, or catch exceptions and raise/log a clear error including the
raw response body; then pass the validated content into _parse_full_log_response
and continue to attach usage/model fields as before. Ensure you reference the
response coming from _openai_request_with_retry, the local variable data, and
the call to _parse_full_log_response when implementing the guard.

In `@error_classification/pr_commentator.py`:
- Around line 294-300: The code calls int(pull_requests[0].get("number")) which
will raise TypeError if "number" is missing; update the extraction to guard
against None by first retrieving the raw value (e.g., num =
pull_requests[0].get("number")), check if num is not None (and optionally
isinstance/isdigit if strings possible), then set pr_num = int(num); if num is
None skip/continue or handle the missing case. Apply the same guard to the other
place that extracts a PR number without checking for None so both branches
consistently validate before calling int().

In `@error_classification/requirements.txt`:
- Around line 5-6: The requirements file uses open-ended version specifiers for
anthropic and requests (anthropic>=0.40.0 and requests>=2.28.0) which violates
the repo policy; update these to bounded ranges or exact pins (for example
change anthropic to a bounded spec like anthropic>=0.40.0,<0.80.0 or an exact
pin, and change requests to a bounded spec like requests>=2.28.0,<3.0.0 or an
exact pin) so the dependency lines referencing "anthropic" and "requests"
conform to the repository's version pinning policy.
🧹 Nitpick comments (15)
CONTRIBUTING.md (1)

239-245: Minor redundancy in the Error Classification section.

"On-demand" (line 244) and "Via diagnose" (line 245) both describe the same thing: commenting /diagnose triggers classification. Consider merging these two bullet points.

Suggested simplification
 - **Automatic** – The error classifier runs as the final step in CI pipelines (`pre-merge`, `pr`, `ci-test-suite`, `container-validation-dynamo`, `copyright-checks`, `build-frontend-image`). When failures are detected, it posts a PR comment categorizing each as **infrastructure error** (network/runner issues) or **code error** (build/test/compilation failures).
-- **On-demand** – Comment `/diagnose` on any PR to run classification along with the full merge-readiness diagnostic.
-- **Via diagnose** – The `/diagnose` command automatically runs the error classifier when CI failures are present.
+- **On-demand** – Comment `/diagnose` on any PR to run the error classifier (when CI failures are present) along with the full merge-readiness diagnostic.
.github/scripts/lgtm_bot_diagnose.py (2)

294-325: Subprocess invocation uses a relative path — depends on cwd being set correctly.

run_error_classifier invokes python3 .github/scripts/classify_workflow_errors.py with cwd=repo_root (where repo_root = os.getcwd() from main()). This works in GitHub Actions where the working directory is the repo checkout, but is fragile if the script is ever invoked from a different directory.

Consider making the path resolution explicit:

Suggested fix
+    script_path = os.path.join(repo_root, ".github", "scripts", "classify_workflow_errors.py")
     result = subprocess.run(
-        ["python3", ".github/scripts/classify_workflow_errors.py"],
+        ["python3", script_path],
         capture_output=True,
         text=True,
         timeout=300,
-        cwd=repo_root,
         env=env,
     )

41-44: Missing env vars crash on startup with an unhelpful KeyError.

Lines 41 and 43 use os.environ["GITHUB_TOKEN"] and int(os.environ["PR_NUMBER"]), which raise KeyError / ValueError respectively if not set. Consider explicit validation with a clear error message, especially since this is a CI entry point where misconfiguration is common.

Suggested improvement
-GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
-GITHUB_REPOSITORY = os.environ.get("GITHUB_REPOSITORY", "ai-dynamo/dynamo")
-PR_NUMBER = int(os.environ["PR_NUMBER"])
+GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
+PR_NUMBER_STR = os.environ.get("PR_NUMBER")
+GITHUB_REPOSITORY = os.environ.get("GITHUB_REPOSITORY", "ai-dynamo/dynamo")
+
+if not GITHUB_TOKEN:
+    print("Error: GITHUB_TOKEN environment variable is required", file=sys.stderr)
+    sys.exit(1)
+if not PR_NUMBER_STR:
+    print("Error: PR_NUMBER environment variable is required", file=sys.stderr)
+    sys.exit(1)
+
+PR_NUMBER = int(PR_NUMBER_STR)
.github/workflows/lgtm-bot-diagnose.yml (2)

49-55: Authorization gate allows first-time contributors — consider tightening for LLM cost control.

The author_association != 'NONE' check (line 55) permits FIRST_TIMER and FIRST_TIME_CONTRIBUTOR to trigger /diagnose, which invokes the NVIDIA Inference API if the secret is configured. On a public repo, this could be abused to run up API costs.

If LLM API usage is a concern, consider restricting to more established associations:

Suggested tightening
       ) &&
-      github.event.comment.author_association != 'NONE'
+      (
+        github.event.comment.author_association == 'MEMBER' ||
+        github.event.comment.author_association == 'OWNER' ||
+        github.event.comment.author_association == 'COLLABORATOR' ||
+        github.event.comment.author_association == 'CONTRIBUTOR'
+      )

67-72: Virtual env created but not persisted — works but fragile across steps.

The venv at /tmp/error-classification-venv is created in one step and activated in the next (line 84). This works because both steps share the same runner filesystem, but the pattern is slightly unusual. A common alternative is to use pip install --user or pip install directly (since setup-python already provides an isolated environment). Minor nit — current approach is functional.

.github/workflows/lgtm-bot-pr-open.yml (3)

91-98: Glob matcher doesn't handle character classes ([...]) from CODEOWNERS.

GitHub CODEOWNERS supports [...] character class patterns (e.g., *.[ch]). The regex escaping on line 93 escapes [ and ] (via [.+^${}()|[\]\\]), which breaks character-class patterns. If your CODEOWNERS file doesn't use character classes, this is fine for now — but worth noting as a gap.


191-194: Biased shuffle for reviewer selection.

sort(() => Math.random() - 0.5) is a well-known biased shuffle — some orderings are more likely than others. For reviewer selection the impact is negligible, but if fairness across team members matters, a Fisher-Yates shuffle is trivial:

♻️ Optional: Fisher-Yates shuffle
              function pickRandom(arr, n) {
-               const shuffled = [...arr].sort(() => Math.random() - 0.5);
+               const shuffled = [...arr];
+               for (let i = shuffled.length - 1; i > 0; i--) {
+                 const j = Math.floor(Math.random() * (i + 1));
+                 [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+               }
                return shuffled.slice(0, n);
              }

356-368: Comment search direction is suboptimal for synchronize events.

With direction: 'desc', the newest comments come first, but the CODEOWNERS marker comment was created at PR open time. On synchronize events, you'll paginate through every page before finding it. Consider searching asc (the default) so the original bot comment is found quickly, or switching to the GraphQL search to avoid pagination altogether.

.github/workflows/classify-workflow-errors-reusable.yml (1)

47-52: || true masks apt-get failures, but the subsequent python3 -m venv would catch it.

The || true on Line 49 swallows failures from both apt-get update and the python3-venv install. Since ubuntu-latest runners already have python3-venv available (and actions/setup-python on Line 43 installs the requested version), this is unlikely to be an issue in practice. Just noting that if the venv module were genuinely missing, the failure would surface on Line 50 instead of Line 49.

error_classification/config.py (1)

45-51: Consider stripping the api_key value, consistent with model handling.

Lines 53–57 explicitly .strip() env vars before the or chain to treat whitespace-only values as unset. The api_key on Line 45 does not strip, so a whitespace-only value (e.g., NVIDIA_INFERENCE_API_KEY=" ") would pass the if not api_key guard and propagate an invalid key to the API client.

Proposed fix
-        api_key = os.getenv("NVIDIA_INFERENCE_API_KEY") or os.getenv(
-            "ANTHROPIC_API_KEY"
-        )
+        api_key = (
+            os.getenv("NVIDIA_INFERENCE_API_KEY", "").strip()
+            or os.getenv("ANTHROPIC_API_KEY", "").strip()
+        )
error_classification/classifier.py (1)

203-211: Multiple datetime.now() calls produce subtly different timestamps for the same classification.

classified_at, first_seen, last_seen, and timestamp are each computed independently. Capture a single timestamp before the loop and reuse it for consistency.

Proposed fix
+        now = datetime.now(timezone.utc).isoformat()
         classifications = []
         for error_data in result.get("errors_found", []):
             ...
                 classification_method="full_log_analysis",
                 model_version=result.get("model_version"),
-                classified_at=datetime.now(timezone.utc).isoformat(),
+                classified_at=now,
                 is_duplicate=False,
                 occurrence_count=1,
-                first_seen=datetime.now(timezone.utc).isoformat(),
-                last_seen=datetime.now(timezone.utc).isoformat(),
+                first_seen=now,
+                last_seen=now,
                 prompt_tokens=result.get("prompt_tokens", 0),
                 completion_tokens=result.get("completion_tokens", 0),
                 cached_tokens=result.get("cached_tokens", 0),
-                timestamp=datetime.now(timezone.utc).isoformat(),
+                timestamp=now,
             )
.github/scripts/classify_workflow_errors.py (1)

142-172: github_token and repo parameters are unused.

fetch_job_steps reads steps directly from the job dict and never uses github_token or repo. If the signature is kept for interface consistency with fetch_job_logs, consider prefixing with _ to signal intent.

error_classification/pr_commentator.py (1)

342-411: error_contexts parameter is accepted but never used in _build_summary_markdown_body.

The parameter is passed through from build_comment_markdown (Lines 114-115, 118-119) but the method body never references it. Either wire it into the output or drop it from the signature to avoid confusion.

error_classification/claude_client.py (2)

109-126: Log truncation logic is duplicated between the Anthropic and OpenAI paths.

Lines 117-123 and 362-367 contain identical truncation code. Consider extracting a shared helper to reduce duplication and ensure both paths stay in sync.

Proposed refactor
+    def _truncate_log(self, job_log: str) -> str:
+        """Truncate log to MAX_LOG_LENGTH, keeping the tail (most relevant for failures)."""
+        if len(job_log) > MAX_LOG_LENGTH:
+            truncated_length = len(job_log) - MAX_LOG_LENGTH
+            return (
+                f"[... truncated first {truncated_length} chars ...]\n\n"
+                + job_log[-MAX_LOG_LENGTH:]
+            )
+        return job_log

Then replace both occurrences with job_log = self._truncate_log(job_log).

Also applies to: 344-367


248-254: Re-raised exceptions lose the original traceback.

Lines 251 and 254 wrap and re-raise exceptions without chaining (from e), which drops the original stack trace and makes debugging harder.

Proposed fix
         except json.JSONDecodeError as e:
-            raise ValueError(f"Invalid JSON in response: {e}")
+            raise ValueError(f"Invalid JSON in response: {e}") from e
 
         except Exception as e:
-            raise ValueError(f"Error parsing response: {e}")
+            raise ValueError(f"Error parsing response: {e}") from e

Comment thread .github/LGTM_BOT.md
Comment on lines +136 to +141
### Optional Variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `LGTM_BOT_REVIEWERS_PER_TEAM` | `1` | Number of individual reviewers to pick per team/roster key |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate "Optional Variables" section.

The LGTM_BOT_REVIEWERS_PER_TEAM variable is already documented in the table at Line 86. This section (lines 136-141) repeats the same information under a second "### Optional Variables" heading.

Proposed fix — remove the duplicate section
-### Optional Variables
-
-| Variable | Default | Purpose |
-|----------|---------|---------|
-| `LGTM_BOT_REVIEWERS_PER_TEAM` | `1` | Number of individual reviewers to pick per team/roster key |
-
 ## Error Categories
📝 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
### Optional Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `LGTM_BOT_REVIEWERS_PER_TEAM` | `1` | Number of individual reviewers to pick per team/roster key |
## Error Categories
🤖 Prompt for AI Agents
In @.github/LGTM_BOT.md around lines 136 - 141, Remove the duplicated "###
Optional Variables" table block that repeats the LGTM_BOT_REVIEWERS_PER_TEAM
entry; specifically, delete the second occurrence of the heading "### Optional
Variables" and its table row that lists `LGTM_BOT_REVIEWERS_PER_TEAM` so only
the original documentation of that variable remains (keep the first table where
the variable is defined).

Comment thread .github/LGTM_BOT.md
Comment on lines +155 to +162
```yaml
permissions:
pull-requests: write # Post/update diagnostic comments
checks: read # Read CI check results
statuses: read # Read commit status checks
contents: read # Read CODEOWNERS, repo files
actions: read # List workflow runs
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Permissions doc says checks: read but workflows use checks: write.

All classify-errors job definitions across the consumed workflows declare checks: write (e.g., ci-test-suite.yml Line 1007, pre-merge.yml Line 169, pr.yaml Line 524). The documentation should reflect the actual requirement.

Proposed fix
 permissions:
   pull-requests: write   # Post/update diagnostic comments
-  checks: read           # Read CI check results
+  checks: write          # Read CI check results, write check annotations
   statuses: read         # Read commit status checks
   contents: read         # Read CODEOWNERS, repo files
   actions: read          # List workflow runs
🤖 Prompt for AI Agents
In @.github/LGTM_BOT.md around lines 155 - 162, The permissions doc currently
lists "checks: read" but the workflows' classify-errors job requires "checks:
write"; update the permissions example in .github/LGTM_BOT.md to use checks:
write to match actual workflows. Locate the permissions block and change checks
from read to write, and ensure the comment still explains that write is needed
for the classify-errors job (referencing the classify-errors job name used in
the workflows).

Comment on lines +96 to +113
if not run_id:
# Fallback to current run ID (for workflow_run trigger)
run_id = os.getenv("GITHUB_RUN_ID")

# If still no run_id and we're in workflow_dispatch mode, find latest failed PR run
if not run_id:
github_event_name = os.getenv("GITHUB_EVENT_NAME")
if github_event_name == "workflow_dispatch":
print("ℹ️ No run_id specified, searching for latest failed PR run...")
run_id = find_latest_failed_pr_run(github_token, repo)

if not all([github_token, repo, run_id]):
print("❌ Missing required GitHub environment variables")
print(f" GITHUB_TOKEN: {'set' if github_token else 'missing'}")
print(f" GITHUB_REPOSITORY: {repo or 'missing'}")
print(f" WORKFLOW_RUN_ID: {os.getenv('WORKFLOW_RUN_ID') or 'not set'}")
print(f" GITHUB_RUN_ID: {os.getenv('GITHUB_RUN_ID') or 'not set'}")
return []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

github_token may be None when passed to find_latest_failed_pr_run.

If GITHUB_TOKEN is not set, github_token is None at Line 90, and it flows into find_latest_failed_pr_run at Line 105 where it's interpolated into the Authorization header as "Bearer None", producing a confusing 401 before falling back to the guard at Line 107. Consider checking github_token earlier or guarding before the call.

Proposed fix
     if not run_id:
         github_event_name = os.getenv("GITHUB_EVENT_NAME")
-        if github_event_name == "workflow_dispatch":
+        if github_event_name == "workflow_dispatch" and github_token and repo:
             print("ℹ️  No run_id specified, searching for latest failed PR run...")
             run_id = find_latest_failed_pr_run(github_token, repo)
📝 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
if not run_id:
# Fallback to current run ID (for workflow_run trigger)
run_id = os.getenv("GITHUB_RUN_ID")
# If still no run_id and we're in workflow_dispatch mode, find latest failed PR run
if not run_id:
github_event_name = os.getenv("GITHUB_EVENT_NAME")
if github_event_name == "workflow_dispatch":
print("ℹ️ No run_id specified, searching for latest failed PR run...")
run_id = find_latest_failed_pr_run(github_token, repo)
if not all([github_token, repo, run_id]):
print("❌ Missing required GitHub environment variables")
print(f" GITHUB_TOKEN: {'set' if github_token else 'missing'}")
print(f" GITHUB_REPOSITORY: {repo or 'missing'}")
print(f" WORKFLOW_RUN_ID: {os.getenv('WORKFLOW_RUN_ID') or 'not set'}")
print(f" GITHUB_RUN_ID: {os.getenv('GITHUB_RUN_ID') or 'not set'}")
return []
if not run_id:
# Fallback to current run ID (for workflow_run trigger)
run_id = os.getenv("GITHUB_RUN_ID")
# If still no run_id and we're in workflow_dispatch mode, find latest failed PR run
if not run_id:
github_event_name = os.getenv("GITHUB_EVENT_NAME")
if github_event_name == "workflow_dispatch" and github_token and repo:
print("ℹ️ No run_id specified, searching for latest failed PR run...")
run_id = find_latest_failed_pr_run(github_token, repo)
if not all([github_token, repo, run_id]):
print("❌ Missing required GitHub environment variables")
print(f" GITHUB_TOKEN: {'set' if github_token else 'missing'}")
print(f" GITHUB_REPOSITORY: {repo or 'missing'}")
print(f" WORKFLOW_RUN_ID: {os.getenv('WORKFLOW_RUN_ID') or 'not set'}")
print(f" GITHUB_RUN_ID: {os.getenv('GITHUB_RUN_ID') or 'not set'}")
return []
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 104-104: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)

🤖 Prompt for AI Agents
In @.github/scripts/classify_workflow_errors.py around lines 96 - 113, The code
may call find_latest_failed_pr_run with github_token == None causing
"Authorization: Bearer None" requests; before invoking find_latest_failed_pr_run
(the block using github_event_name and run_id), ensure github_token is present
or return early — e.g., check github_token (and repo) alongside run_id or move
the existing all([github_token, repo, run_id]) guard above the workflow_dispatch
branch; alternatively, add a defensive check inside find_latest_failed_pr_run to
bail when github_token is falsy before building the Authorization header.

Comment on lines +505 to +508
# Token usage summary
total_prompt_tokens = sum(c.prompt_tokens for c in classifications)
total_completion_tokens = sum(c.completion_tokens for c in classifications)
total_cached_tokens = sum(c.cached_tokens for c in classifications)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Token usage totals will be inflated due to per-error duplication in classifier.py.

sum(c.prompt_tokens for c in classifications) aggregates tokens that are duplicated across all ErrorClassification objects from the same API call (see error_classification/classifier.py Lines 208-210). Once that upstream issue is resolved, this aggregation will be correct.

🤖 Prompt for AI Agents
In @.github/scripts/classify_workflow_errors.py around lines 505 - 508, The
totals are inflated because the list classifications contains multiple
ErrorClassification entries per single API call; change the aggregation to sum
tokens once per unique API call instead of per classification. Build a map keyed
by a stable identifier on ErrorClassification (e.g., an api_call_id or
request_id attribute; if not available, use a fallback unique tuple like
(prompt_text, completion_text, timestamp)) and then compute total_prompt_tokens,
total_completion_tokens, and total_cached_tokens by summing the values from each
unique key rather than summing across classifications directly; update the code
around the variables total_prompt_tokens, total_completion_tokens,
total_cached_tokens to use this deduplicated collection.

Comment on lines +96 to +108
// Check for recent nudge to avoid spam
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 10,
direction: 'desc',
});

const recentNudge = comments.find(c =>
c.body && c.body.includes(NUDGE_MARKER) &&
(now - new Date(c.created_at)) / MS_PER_HOUR < NUDGE_COOLDOWN_HOURS
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

direction: 'desc' is silently ignored — deduplication may fail on busy PRs.

The GitHub REST API for listing comments on a specific issue (GET /repos/{owner}/{repo}/issues/{issue_number}/comments) does not support the direction parameter. Comments are always returned oldest-first. With per_page: 10, this fetches the 10 oldest comments, so a recent nudge marker would be missed on PRs with 10+ comments, causing duplicate nudges within the 24h cooldown.

The same issue applies to the merge reminder dedup check at lines 160-166.

Proposed fix — fetch last page instead
-                const { data: comments } = await github.rest.issues.listComments({
-                  owner: context.repo.owner,
-                  repo: context.repo.repo,
-                  issue_number: prNumber,
-                  per_page: 10,
-                  direction: 'desc',
-                });
+                // GitHub's per-issue comment endpoint always returns oldest-first.
+                // Fetch all and search from end to find recent nudge markers.
+                const allComments = await github.paginate(
+                  github.rest.issues.listComments,
+                  {
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    issue_number: prNumber,
+                    per_page: 100,
+                  }
+                );
+                const comments = allComments;
🤖 Prompt for AI Agents
In @.github/workflows/lgtm-bot-stale.yml around lines 96 - 108, The comments
listing currently uses an unsupported direction parameter and requests the 10
oldest comments, causing the recent-nudge dedup (recentNudge using NUDGE_MARKER,
per_page, MS_PER_HOUR, NUDGE_COOLDOWN_HOURS) to miss recent nudges on busy PRs;
update the logic to fetch a larger page (e.g., per_page: 100) and search from
the end (or take the last N entries) to find NUDGE_MARKER within the cooldown
window, and apply the same change to the merge reminder dedup check (the
equivalent recentMergeReminder / MERGE_REMINDER_MARKER logic) so you reliably
examine the most recent comments instead of the oldest.

Comment on lines +72 to +80
# Initialize appropriate client based on API format
if config.api_format == "openai":
# For OpenAI-compatible APIs (like NVIDIA)
self.client = None # Will use requests directly
self.api_base_url = config.api_base_url or "https://api.openai.com/v1"
else:
# For native Anthropic API
self.client = Anthropic(api_key=config.api_key)
self.api_base_url = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Defaulting api_base_url to https://api.openai.com/v1 may silently send requests to the wrong API.

When config.api_format == "openai" but config.api_base_url is None, Line 76 falls back to https://api.openai.com/v1. Since the default config is designed around NVIDIA's inference API (and from_env always sets api_base_url), this fallback is unlikely to trigger in practice — but if it does, it would send an NVIDIA API key to OpenAI's endpoint, producing a confusing auth error. Consider raising an error or using the NVIDIA default here as well.

Proposed fix
         if config.api_format == "openai":
             # For OpenAI-compatible APIs (like NVIDIA)
             self.client = None  # Will use requests directly
-            self.api_base_url = config.api_base_url or "https://api.openai.com/v1"
+            self.api_base_url = config.api_base_url or "https://inference-api.nvidia.com/v1"
📝 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
# Initialize appropriate client based on API format
if config.api_format == "openai":
# For OpenAI-compatible APIs (like NVIDIA)
self.client = None # Will use requests directly
self.api_base_url = config.api_base_url or "https://api.openai.com/v1"
else:
# For native Anthropic API
self.client = Anthropic(api_key=config.api_key)
self.api_base_url = None
# Initialize appropriate client based on API format
if config.api_format == "openai":
# For OpenAI-compatible APIs (like NVIDIA)
self.client = None # Will use requests directly
self.api_base_url = config.api_base_url or "https://inference-api.nvidia.com/v1"
else:
# For native Anthropic API
self.client = Anthropic(api_key=config.api_key)
self.api_base_url = None
🤖 Prompt for AI Agents
In `@error_classification/claude_client.py` around lines 72 - 80, The current
constructor branch that handles config.api_format == "openai" silently defaults
self.api_base_url to "https://api.openai.com/v1" which can send the wrong API
key; instead, update the initialization in claude_client.py so that when
config.api_format == "openai" and config.api_base_url is None you raise a clear
exception (e.g., ValueError) telling the caller to provide api_base_url (or
explicitly choose the correct NVIDIA default) rather than silently falling back;
change the code around the block that sets self.client and self.api_base_url
(the if config.api_format == "openai" branch) to validate config.api_base_url
and raise an error with a descriptive message if it's missing.

Comment on lines +383 to +399
response = self._openai_request_with_retry(payload)
data = response.json()

# Parse OpenAI format response
content = data["choices"][0]["message"]["content"]
usage = data.get("usage", {})

# Parse JSON from response
result = self._parse_full_log_response(content)

# Add token usage
result["prompt_tokens"] = usage.get("prompt_tokens", 0)
result["completion_tokens"] = usage.get("completion_tokens", 0)
result["cached_tokens"] = usage.get("cached_tokens", 0)
result["model_version"] = data.get("model", self.config.model)

return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unguarded chained index access on OpenAI response can crash with a confusing error.

data["choices"][0]["message"]["content"] on Line 387 will raise KeyError or IndexError if the API response is malformed (e.g., an error body returned with a 200, or an empty choices array). A defensive check or a try/except with a clear message would improve debuggability.

Proposed fix
         response = self._openai_request_with_retry(payload)
         data = response.json()
 
-        # Parse OpenAI format response
-        content = data["choices"][0]["message"]["content"]
+        # Parse OpenAI format response (with defensive access)
+        try:
+            content = data["choices"][0]["message"]["content"]
+        except (KeyError, IndexError, TypeError) as exc:
+            raise ValueError(
+                f"Unexpected API response structure: {str(data)[:300]}"
+            ) from exc
         usage = data.get("usage", {})
🤖 Prompt for AI Agents
In `@error_classification/claude_client.py` around lines 383 - 399, The code
currently assumes the OpenAI response has choices[0].message.content and will
throw KeyError/IndexError; wrap the extraction of content (after calling
_openai_request_with_retry and parsing response.json()) in a defensive check or
try/except: verify data is a dict, that "choices" exists and is a non-empty list
and that choices[0] has "message" and "content" keys, or catch exceptions and
raise/log a clear error including the raw response body; then pass the validated
content into _parse_full_log_response and continue to attach usage/model fields
as before. Ensure you reference the response coming from
_openai_request_with_retry, the local variable data, and the call to
_parse_full_log_response when implementing the guard.

Comment on lines +294 to +300
pull_requests = workflow_run.get("pull_requests", [])
print(f" pull_requests array length: {len(pull_requests)}")

if pull_requests and len(pull_requests) > 0:
pr_num = int(pull_requests[0].get("number"))
print(f" ✅ Found PR #{pr_num} from workflow_run.pull_requests")
return pr_num

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

int(pull_requests[0].get("number")) will raise TypeError if "number" is absent.

dict.get("number") returns None when the key is missing, and int(None) raises TypeError. Line 331 has the opposite problem (no int() coercion). Both should guard for None.

Proposed fix
                     if pull_requests and len(pull_requests) > 0:
-                        pr_num = int(pull_requests[0].get("number"))
-                        print(f"  ✅ Found PR #{pr_num} from workflow_run.pull_requests")
-                        return pr_num
+                        raw_pr_num = pull_requests[0].get("number")
+                        if raw_pr_num is not None:
+                            pr_num = int(raw_pr_num)
+                            print(f"  ✅ Found PR #{pr_num} from workflow_run.pull_requests")
+                            return pr_num
🤖 Prompt for AI Agents
In `@error_classification/pr_commentator.py` around lines 294 - 300, The code
calls int(pull_requests[0].get("number")) which will raise TypeError if "number"
is missing; update the extraction to guard against None by first retrieving the
raw value (e.g., num = pull_requests[0].get("number")), check if num is not None
(and optionally isinstance/isdigit if strings possible), then set pr_num =
int(num); if num is None skip/continue or handle the missing case. Apply the
same guard to the other place that extracts a PR number without checking for
None so both branches consistently validate before calling int().

Comment on lines +5 to +6
anthropic>=0.40.0
requests>=2.28.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the requirements.txt file
find . -name "requirements.txt" -type f | head -20

Repository: ai-dynamo/dynamo

Length of output: 351


🏁 Script executed:

# Read the specific file mentioned in the review
cat -n error_classification/requirements.txt

Repository: ai-dynamo/dynamo

Length of output: 321


🏁 Script executed:

# Check for version pinning patterns in other requirements files
find . -name "requirements*.txt" -type f -exec head -15 {} + 2>/dev/null | head -60

Repository: ai-dynamo/dynamo

Length of output: 2954


🌐 Web query:

anthropic python package version 0.40.0 pypi

💡 Result:

PyPI: anthropic Python package — version 0.40.0

  • Install: pip install anthropic==0.40.0 [1]
  • Release date: Nov 28, 2024 [1]
  • Summary on PyPI: “The official Python library for the anthropic API” [1]
  • Python requirement: Python >= 3.8 [1]
  • Note: PyPI indicates a newer version is available (e.g., 0.79.0 uploaded Feb 7, 2026) [1][2]

Sources: [1] [2]


Update dependency versions to comply with the repository's version pinning policy.

The repository has a documented requirement (see container/deps/requirements.txt) that explicitly prohibits open-ended >= operators, stating they "allow untested future versions that may introduce breaking changes, create non-reproducible builds, and cause dependency conflicts."

Both dependencies must be updated:

  • anthropic>=0.40.0 should use bounded syntax (e.g., anthropic>=0.40.0,<0.80.0) or pinned exactly
  • requests>=2.28.0 should use bounded syntax or pinned exactly

The anthropic SDK has rapid version churn (0.40.0 released Nov 2024, but 0.79.0 now available as of Feb 2026), making bounded ranges especially important here.

🤖 Prompt for AI Agents
In `@error_classification/requirements.txt` around lines 5 - 6, The requirements
file uses open-ended version specifiers for anthropic and requests
(anthropic>=0.40.0 and requests>=2.28.0) which violates the repo policy; update
these to bounded ranges or exact pins (for example change anthropic to a bounded
spec like anthropic>=0.40.0,<0.80.0 or an exact pin, and change requests to a
bounded spec like requests>=2.28.0,<3.0.0 or an exact pin) so the dependency
lines referencing "anthropic" and "requests" conform to the repository's version
pinning policy.

@dillon-cullinan

dillon-cullinan commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

/lgtm-bot diagnose

I would like to see the resulting comment generated by the diagnosis with my pending review comments

- Paginate reviews in lgtm-bot.yml (main + synchronize paths) and
  lgtm_bot_diagnose.py to handle PRs with 30+ review entries
- Add timeout=30 to all HTTP calls missing timeouts (github_request
  urlopen, pr_commentator patch/post/get)
- Remove unsupported direction=desc parameter from issue comments
  endpoint in 3 locations (GitHub API silently ignores it)
- Suppress Slack webhook URL in curl error output (2>/dev/null)
- Fix RateLimiter stale timestamp: use time.time() after sleep
  instead of pre-sleep value
- Guard int() on MAX_RPM env var with clear error message
- Fix token count inflation: assign API call tokens only to first
  ErrorClassification to avoid multiplied totals in aggregates
- Add int() coercion to _find_pr_by_commit return value

Signed-off-by: Dan Gil <dagil@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

@dillon-cullinan Good question — here is what this gives us beyond the GitHub UI:

  1. Single merge-readiness view. GitHub shows CI, reviews, and branch protection as separate signals across different tabs. The bot combines them into one checklist comment that updates automatically, so contributors (especially external ones unfamiliar with our setup) can see exactly what is blocking their PR without navigating branch protection settings.

  2. lgtm label for automation. The label provides a machine-readable "ready to merge" signal that external merge automation or dashboards can key off. GitHub has no equivalent of a single label that gates on all of: required CI, approvals, no outstanding changes requested, and branch protection.

  3. LLM-powered CI failure diagnosis. /diagnose fetches failed job logs and runs them through an LLM to produce root cause analysis and fix suggestions — the GitHub UI just shows raw logs. This is particularly valuable for external contributors who do not know our CI infrastructure and cannot distinguish infra flakes from real failures.

  4. CODEOWNERS visibility. The Required Reviewers comment shows contributors which teams need to review and why, based on CODEOWNERS rules. GitHub shows this only to repo admins in branch protection settings — contributors see "review required" with no indication of who or why.

  5. Scale. With 250+ open PRs and growing external contributor volume, the bot reduces maintainer toil: auto-requesting the right reviewers, nudging stale reviews, and giving contributors self-service diagnostics instead of waiting for a maintainer to look at their CI.

The CONTRIBUTING.md update documents these features for contributors so they know what to expect.

@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/lgtm-bot diagnose

I would like to see the resulting comment generated by the diagnosis with my pending review comments

OK - I'll see why that's not working

@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

and sorry for the dramatic increase in size - Nate's PR gave me a lot of idea. I didn't realize you were actively reviewing

@dagil-nvidia

Copy link
Copy Markdown
Collaborator Author

/diagnose

@dagil-nvidia
dagil-nvidia marked this pull request as draft February 14, 2026 09:14
@dillon-cullinan

dillon-cullinan commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

@dillon-cullinan Good question — here is what this gives us beyond the GitHub UI:

1. **Single merge-readiness view.** GitHub shows CI, reviews, and branch protection as separate signals across different tabs. The bot combines them into one checklist comment that updates automatically, so contributors (especially external ones unfamiliar with our setup) can see exactly what is blocking their PR without navigating branch protection settings.

This is all available just on the PR alone. Its not across separate tabs, its all on the same page. It is all completely viewable within the Conversation tab on Github, all specifically designed to always be visible on the page.

This functionality is just attempting to recreate the Github UI to post an extremely long comment that is very easily lost in history and becomes useless in a PR. In fact, its possible this comment might even hit a character limit. The number of lines used to "build the comment" in the python script is nearing 200. That is an extremely large comment.

Additionally, this PR makes countless API requests. Do you know what the API request limit for the token is? And do you have any guesstimate on how many API calls this is doing per hour if it were to be implemented across the repository?

2. **`lgtm` label for automation.** The label provides a machine-readable "ready to merge" signal that external merge automation or dashboards can key off. GitHub has no equivalent of a single label that gates on all of: required CI, approvals, no outstanding changes requested, and branch protection.

This functionality I will admit has some usefulness, but also could've been done in about 50 lines of code, and not thousands.

3. **LLM-powered CI failure diagnosis.** `/diagnose` fetches failed job logs and runs them through an LLM to produce root cause analysis and fix suggestions — the GitHub UI just shows raw logs. This is particularly valuable for external contributors who do not know our CI infrastructure and cannot distinguish infra flakes from real failures.

This PR is not doing that currently, that is a separate branch and a separate PR with what Nate is doing, of which I'm also trying to tone down heavily what its doing. This PR should be rebased onto main because this is far beyond what is reasonably reviewable. I'm reviewing it anyway to make a point at how much time is spent reviewing largely AI generated code with little human involvement. It is unmaintainable, duplicates code everywhere, doesn't use proper tools and functionality, its extremely bloated and not friendly to read, doesn't consider edge cases, and even has security vulnerabilities.

Before the code was merged with Nates, there was a small section using Nvidia's inference service to diagnose logs, of which I'm questioning how useful this is anyway. What is the point of diagnosing a Lint Checker? It already tells you exactly how to fix things.

4. **CODEOWNERS visibility.** The Required Reviewers comment shows contributors which teams need to review and why, based on CODEOWNERS rules. GitHub shows this only to repo admins in branch protection settings — contributors see "review required" with no indication of _who_ or _why_.

Why does it matter who or why? That is only our concern and not the external contributor. If we are not keeping up with reviews that is a problem with our process, not a problem that needs to be splattered as a comment on every PR after every commit. If our codeowners are hard to understand then we should fix codeowners, not write more code that explains our complicated codeowners.

5. **Scale.** With 250+ open PRs and growing external contributor volume, the bot reduces maintainer toil: auto-requesting the right reviewers, nudging stale reviews, and giving contributors self-service diagnostics instead of waiting for a maintainer to look at their CI.

I don't believe this functionality scales at all, actually. I predict that it will have various problems.

The CONTRIBUTING.md update documents these features for contributors so they know what to expect.

Couldn't we have just documented what the requirements to merge a pull request are instead of doing this then?

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

Labels

actions ci Issues/PRs that reference CI build/test documentation Improvements or additions to documentation size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants