Skip to content

[ci] Added LLM based transient error re-run mechanism#663

Merged
nemesifier merged 11 commits into
masterfrom
refactor/transient-rerun-mechanism
May 4, 2026
Merged

[ci] Added LLM based transient error re-run mechanism#663
nemesifier merged 11 commits into
masterfrom
refactor/transient-rerun-mechanism

Conversation

@stktyagi

@stktyagi stktyagi commented May 3, 2026

Copy link
Copy Markdown
Member

Added LLM based transient error re-run mechanism and kept it optional with current implementation.

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Description of Changes

Kept the LLM retry logic as the default and kept current implementation optional along with it. The LLM mode uses a strict yes/no prompt and only touches the retry marker, while the original analysis flow stays unchanged.

Added LLM based transient error re-run mechanism and kept it optional with current implementation.
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds an LLM-driven CI retry classifier to the CI failure analyzer. Two functions were introduced: _parse_retry_decision(text) to interpret a Gemini response as YES/NO (or None), and _should_retry_ci(client, error_log, model, tag_id) which prompts Gemini with untrusted failure logs and returns a boolean retry decision (safe-failing to False). main() now reads CI_RETRY_MODE and GEMINI_MODEL earlier, combines transient-only detection and the LLM classifier per mode, and writes the transient_failure marker only when the combined should_retry is true. Tests were extended to cover parsing and LLM retry-mode flows.

Sequence Diagram(s)

sequenceDiagram
    participant GH as GitHub Actions
    participant AF as analyze_failure.py
    participant Gemini as Gemini API
    participant FS as File System

    GH->>AF: Invoke analyze_failure.py with logs & env
    AF->>AF: Read CI_RETRY_MODE and GEMINI_MODEL

    alt CI_RETRY_MODE == "llm" or "both"
        AF->>Gemini: _should_retry_ci(error_log, model, tag_id)
        Gemini-->>AF: Short reply (first line)
        AF->>AF: _parse_retry_decision(first_line)
    end

    AF->>AF: Compute transient_only detection
    AF->>AF: Combine decisions per retry_mode -> should_retry

    alt should_retry == true
        AF->>FS: Write "transient_failure" marker
        FS-->>AF: Marker created
    else
        AF->>AF: Do not write marker
    end

    AF->>Gemini: Generate full analysis report (GEMINI_MODEL)
    Gemini-->>AF: Full analysis
    AF-->>GH: Print analysis and retry outcome
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Bug Fixes ❓ Inconclusive Unable to verify if the proposed bug fix changing the LLM prompt default from 'If unsure answer YES' to 'If unsure answer NO' was actually implemented in the code. Access the analyze_failure.py file to confirm the prompt change was applied and verify the regression test properly validates this fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description follows the required template structure with all major sections completed: checklist items marked, reference section present, and changes described. However, documentation updates are not checked.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title clearly summarizes the main change: adding an LLM-based mechanism for handling transient errors, which aligns with the file changes and PR objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/transient-rerun-mechanism

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 added enhancement github_actions Pull requests that update GitHub Actions code helper-bots Helper bots, release management automation labels May 3, 2026
@kilo-code-bot

kilo-code-bot Bot commented May 3, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

The PR introduces a well-designed LLM-based retry classification system with three configurable modes:

  • llm (default): Uses LLM to decide retry
  • both: Combines transient markers with LLM decision
  • Fallback: Uses only transient markers

Security considerations are properly handled with unique XML-like tags for prompt injection protection. Error handling is comprehensive with graceful fallbacks. Test coverage is thorough with new tests for the retry logic and updates to existing tests.

Incremental Updates:

  • Added explicit CI_RETRY_MODE environment variables to test cases for improved test determinism
  • Updated workflow to use refactor/transient-rerun-mechanism branch for testing the reusable workflow (expected temporary change for PR validation)
  • Added debug logging for retry classifier output and decision tracking (model output preview, mode/state visibility)
  • Exposed CI_RETRY_MODE as a GitHub Variable in the reusable workflow for runtime configuration
  • Fixed test assertions to accommodate debug logging (assert_called_once_with to assert_any_call, assertEqual to assertGreaterEqual for call counts)
  • Updated trusted_scripts checkout to use refactor/transient-rerun-mechanism branch for testing (temporary change)
  • Fixed default value handling for CI_RETRY_MODE to properly handle empty string values
  • Refined LLM decision rules for clearer YES/NO guidance (clarified transient vs deterministic failures)
Files Reviewed (4 files)
  • .github/actions/bot-ci-failure/analyze_failure.py - Added _parse_retry_decision() and _should_retry_ci() functions, retry mode logic, debug logging, fixed default value handling, and refined LLM system instructions
  • .github/actions/bot-ci-failure/test_analyze_failure.py - Added TestParseRetryDecision class, updated existing tests with explicit CI_RETRY_MODE env vars and flexible assertions for debug logging
  • .github/workflows/bot-ci-failure.yml - Updated reusable workflow branch reference for testing
  • .github/workflows/reusable-bot-ci-failure.yml - Added CI_RETRY_MODE environment variable from GitHub Variables, updated trusted_scripts checkout branch for testing

Reviewed by kimi-k2.5 · 157,970 tokens

@openwisp-companion

Copy link
Copy Markdown

Test Failures in CI

Hello @stktyagi,
(Analysis for commit 5a6cc97)

  • Test Failure: The test test_transient_failure_creates_marker_file in .github/actions/bot-ci-failure/test_analyze_failure.py failed because the open function was not called with the expected arguments ('transient_failure', 'w'). This indicates that the main() function in analyze_failure.py did not correctly trigger the file writing operation as expected by the test.

To fix this, review the logic within the main() function in analyze_failure.py to ensure that when a transient failure is detected, the open('transient_failure', 'w') call is correctly executed. Check the conditions that lead to this file operation and ensure they are met during the test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/actions/bot-ci-failure/test_analyze_failure.py (1)

777-803: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test will fail: mode mismatch between assertion and mock setup.

This test expects the marker file to be created when transient_only=True, but:

  1. No CI_RETRY_MODE is set, so it defaults to "llm"
  2. In "llm" mode, should_retry depends only on _should_retry_ci() (lines 404-405 in main)
  3. mock_retry.return_value = False, so should_retry = False
  4. The marker file won't be created

This contradicts the test at line 848-864 (test_retry_mode_llm_no_retry_when_llm_says_no) which correctly asserts mock_file.assert_not_called() for the same scenario.

🐛 Proposed fix: set CI_RETRY_MODE to preserve old behavior
 `@patch.dict`(
     os.environ,
-    {"GEMINI_API_KEY": "fake_key", "PR_AUTHOR": "test", "COMMIT_SHA": "abc"},
+    {"GEMINI_API_KEY": "fake_key", "PR_AUTHOR": "test", "COMMIT_SHA": "abc", "CI_RETRY_MODE": "transient_only"},
 )
 def test_transient_failure_creates_marker_file(
     self, mock_retry, mock_repo, mock_logs, mock_genai, mock_print, mock_file
 ):
     mock_logs.return_value = ("marionette error", False, True)
-    mock_retry.return_value = False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/bot-ci-failure/test_analyze_failure.py around lines 777 -
803, Test fails because CI_RETRY_MODE defaults to "llm" so main() uses
_should_retry_ci() (mocked False) and no marker file is written; fix by ensuring
the test forces the retry mode to transient-only: update the `@patch.dict` for
environment in test_transient_failure_creates_marker_file to include
"CI_RETRY_MODE": "transient-only" (or alternatively make mock_retry return True)
so main() computes should_retry as True and the "transient_failure" marker is
created when calling main() (references:
test_transient_failure_creates_marker_file, main, _should_retry_ci,
CI_RETRY_MODE).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/bot-ci-failure/analyze_failure.py:
- Around line 303-311: The _parse_retry_decision function can IndexError when
text is whitespace-only because text.strip().splitlines() can be empty; fix by
first computing stripped = text.strip() and returning None if not stripped (or
checking splitlines length) before accessing [0], then proceed to derive first =
stripped.splitlines()[0].strip().upper() and the existing startswith checks to
return True/False/None.

---

Outside diff comments:
In @.github/actions/bot-ci-failure/test_analyze_failure.py:
- Around line 777-803: Test fails because CI_RETRY_MODE defaults to "llm" so
main() uses _should_retry_ci() (mocked False) and no marker file is written; fix
by ensuring the test forces the retry mode to transient-only: update the
`@patch.dict` for environment in test_transient_failure_creates_marker_file to
include "CI_RETRY_MODE": "transient-only" (or alternatively make mock_retry
return True) so main() computes should_retry as True and the "transient_failure"
marker is created when calling main() (references:
test_transient_failure_creates_marker_file, main, _should_retry_ci,
CI_RETRY_MODE).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aba13696-af1e-43c7-a352-da4b6da86ec1

📥 Commits

Reviewing files that changed from the base of the PR and between 83ac9fc and 5a6cc97.

📒 Files selected for processing (2)
  • .github/actions/bot-ci-failure/analyze_failure.py
  • .github/actions/bot-ci-failure/test_analyze_failure.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Kilo Code Review
🔇 Additional comments (6)
.github/actions/bot-ci-failure/analyze_failure.py (2)

314-363: LGTM!

The retry classifier implementation is well-structured:

  • Security tagging with random tag_id to prevent prompt injection
  • Appropriate temperature (0.2) and token limit for deterministic yes/no responses
  • Safe fallback to no-retry on errors or invalid output

400-415: LGTM!

The retry mode logic is clean and covers the three documented modes appropriately:

  • llm: uses only LLM decision
  • both: transient_only OR llm for maximum retry coverage
  • fallback: uses existing transient_only logic

Model selection moved earlier for reuse across both retry classification and analysis is a good refactor.

.github/actions/bot-ci-failure/test_analyze_failure.py (4)

14-14: LGTM!

Import added for the new _parse_retry_decision function being tested.


535-549: LGTM!

Test coverage for _parse_retry_decision handles the core cases. Consider adding a test for whitespace-only input (e.g., " ") once the IndexError bug is fixed.


576-604: LGTM!

The mock_retry patches correctly isolate tests from the new _should_retry_ci function. Setting mock_retry.return_value = False is appropriate for tests focused on other aspects of main().


805-897: LGTM!

The new LLM retry mode tests correctly validate:

  • test_llm_retry_creates_marker_file: LLM says YES → marker created
  • test_retry_mode_llm_no_retry_when_llm_says_no: LLM says NO → no marker (even with transient_only=True)
  • test_retry_mode_llm_uses_llm_decision: verifies LLM decision drives marker creation

These tests properly cover the new retry classification behavior.

Comment thread .github/actions/bot-ci-failure/analyze_failure.py
stktyagi added 3 commits May 3, 2026 16:34
Added CI_RETRY_MODE parameter to prevent test failure
Changed ref from master to testing branch
Added notices in ci to check what mode is working
@openwisp-companion

Copy link
Copy Markdown

Multiple Test Failures in test_analyze_failure.py

Hello @stktyagi,
(Analysis for commit e73588c)

There are multiple test failures in test_analyze_failure.py, all related to mock_print.assert_called_once_with or self.assertEqual(mock_print.call_count, 1). The common issue is that the print function is being called twice when the tests expect it to be called only once.

Specifically, the failures are in:

  • test_fails_format_validation
  • test_handles_api_exception
  • test_handles_empty_api_response
  • test_strips_wrapping_code_fences
  • test_successful_api_call_prints_response
  • test_truncates_large_api_response

Fix:
Review the logic within these test cases to ensure that print is called exactly once when asserting assert_called_once_with or when checking call_count == 1. It appears that in each failing test, a ::notice message is printed in addition to the expected output, leading to the second call. Adjust the test mocks or the tested code's print statements to align with the expected single call.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
.github/actions/bot-ci-failure/analyze_failure.py (1)

303-311: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard whitespace-only classifier output before indexing the first line.

" \n" passes the if not text check, then Line 306 and Line 358 both do splitlines()[0] on an empty list. In the default llm path that turns an invalid classifier response into an unhandled IndexError instead of a safe False.

Suggested fix
 def _parse_retry_decision(text):
     if not text:
         return None
-    first = text.strip().splitlines()[0].strip().upper()
+    stripped = text.strip()
+    if not stripped:
+        return None
+    first = stripped.splitlines()[0].strip().upper()
     if first.startswith("YES"):
         return True
     if first.startswith("NO"):
         return False
     return None
@@
-    preview = raw_text.strip().splitlines()[0] if raw_text else "<empty>"
+    stripped = raw_text.strip() if raw_text else ""
+    preview = stripped.splitlines()[0] if stripped else "<empty>"

Also applies to: 356-358

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/bot-ci-failure/analyze_failure.py around lines 303 - 311,
The _parse_retry_decision function fails on whitespace-only text because it
checks only `if not text` then immediately does `splitlines()[0]`; update it to
guard for whitespace-only input by trimming and verifying there is at least one
non-empty line before indexing (e.g., compute stripped = text.strip(); if not
stripped: return None; then take first = stripped.splitlines()[0].upper() and
proceed). Apply the same defensive check to the other identical
classifier-handling block referenced around the second occurrence so both paths
return None for blank/whitespace-only classifier output instead of raising
IndexError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/bot-ci-failure/analyze_failure.py:
- Around line 406-417: The code reads CI_RETRY_MODE into retry_mode and branches
based on "llm"/"both"/other, but the workflow wiring doesn't export
CI_RETRY_MODE so the default "llm" is always used; update the reusable workflow
environment block to export CI_RETRY_MODE from a repository variable or input
(same place GEMINI_MODEL is exported) so the branch in retry_mode sees the
intended value; ensure the env key name matches exactly ("CI_RETRY_MODE") and
preserve the existing default behavior in analyze_failure.py by keeping
os.environ.get("CI_RETRY_MODE", "llm").

In @.github/workflows/bot-ci-failure.yml:
- Line 75: The reusable workflow reference is pinned to a feature branch in the
`uses:
openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@refactor/transient-rerun-mechanism`
line; update that reference to a stable ref (e.g., `@master` or a specific
commit SHA) so the CI bot always uses a post-merge, stable version instead of a
transient feature branch.

---

Duplicate comments:
In @.github/actions/bot-ci-failure/analyze_failure.py:
- Around line 303-311: The _parse_retry_decision function fails on
whitespace-only text because it checks only `if not text` then immediately does
`splitlines()[0]`; update it to guard for whitespace-only input by trimming and
verifying there is at least one non-empty line before indexing (e.g., compute
stripped = text.strip(); if not stripped: return None; then take first =
stripped.splitlines()[0].upper() and proceed). Apply the same defensive check to
the other identical classifier-handling block referenced around the second
occurrence so both paths return None for blank/whitespace-only classifier output
instead of raising IndexError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2e0508d-8a06-4fa2-95db-341df4d2deee

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6cc97 and e73588c.

📒 Files selected for processing (3)
  • .github/actions/bot-ci-failure/analyze_failure.py
  • .github/actions/bot-ci-failure/test_analyze_failure.py
  • .github/workflows/bot-ci-failure.yml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-24T16:40:12.455Z
Learnt from: stktyagi
Repo: openwisp/openwisp-utils PR: 631
File: .github/workflows/bot-ci-failure.yml:71-74
Timestamp: 2026-03-24T16:40:12.455Z
Learning: When reviewing .github/workflows/bot-ci-failure.yml, treat the reusable-bot-ci-failure.yml auto-retry step as authenticated via a custom GitHub App token passed through GH_TOKEN (generated from APP_ID and PRIVATE_KEY secrets and used by the gh CLI). Because this uses the App token (not the workflow’s GITHUB_TOKEN), workflow permissions settings (the `permissions` block, which only restricts GITHUB_TOKEN) do not apply to the `gh api -X POST` call. Therefore, do not flag the `gh` POST as a permissions problem if `actions: read` is set in the reusable workflow.

Applied to files:

  • .github/workflows/bot-ci-failure.yml

Comment thread .github/actions/bot-ci-failure/analyze_failure.py Outdated
actions: write
contents: read
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@master
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@refactor/transient-rerun-mechanism

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't leave the reusable workflow pinned to a feature branch.

After this merges, master will still call reusable-bot-ci-failure.yml from refactor/transient-rerun-mechanism. If that branch is deleted or drifts, the CI failure bot on master starts running stale or broken logic. Point this back to the post-merge stable ref (or a commit SHA if you need a temporary pin during validation).

Suggested change
-    uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@refactor/transient-rerun-mechanism
+    uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@master
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@refactor/transient-rerun-mechanism
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@master
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/bot-ci-failure.yml at line 75, The reusable workflow
reference is pinned to a feature branch in the `uses:
openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@refactor/transient-rerun-mechanism`
line; update that reference to a stable ref (e.g., `@master` or a specific
commit SHA) so the CI bot always uses a post-merge, stable version instead of a
transient feature branch.

CI_RETRY_MODE was configurable in code but not reachable from the workflow wiring.
@openwisp-companion

Copy link
Copy Markdown

Test Failures in test_analyze_failure.py

Hello @stktyagi,
(Analysis for commit 3695720)

There are 6 test failures in test_analyze_failure.py. The common pattern is that print is called twice when it's expected to be called only once. This indicates an issue with how mock objects are being used or how the test assertions are set up.

Here's a breakdown of the failures and how to fix them:

  1. TestMain.test_fails_format_validation:
  • Failure: AssertionError: Expected 'print' to be called once. Called 2 times.
  • Fix: Review the test logic to ensure print is called only once within the test_fails_format_validation test case. It seems like an extra print call is happening, possibly due to unexpected behavior in the mocked genai or get_error_logs functions.
  1. TestMain.test_handles_empty_api_response:
  • Failure: AssertionError: Expected 'print' to be called once. Called 2 times.
  • Fix: The test expects print to be called once with a specific warning message. However, the logs show it's called twice: once with a notice and once with the warning. Adjust the test's expected calls or the mocked functions to match the desired single print call.
  1. TestMain.test_strips_wrapping_code_fences:
  • Failure: AssertionError: Expected 'print' to be called once. Called 2 times.
  • Fix: Similar to the above, the test expects a single print call, but two are occurring. Examine the analyze_failure.main function's logic and the mocks for _should_retry_ci, get_repo_context, get_error_logs, and genai to identify the source of the second print call.
  1. TestMain.test_successful_api_call_prints_response:
  • Failure: AssertionError: Expected 'print' to be called once. Called 2 times.
  • Fix: The test asserts that print is called exactly once. The failure indicates it's called twice. This might be due to an issue in how the genai mock is configured or how the main function handles the response. Ensure only one print call is made with the expected arguments.
  1. TestMain.test_truncates_large_api_response:
  • Failure: AssertionError: 2 != 1
  • Fix: This test asserts that mock_print.call_count is 1, but it's actually 2. This points to the same underlying issue as the other print call failures. Review the test setup and mocks to ensure print is invoked only once.
  1. TestMain.test_handles_api_exception:
  • Failure: AssertionError: Expected 'print' to be called once. Called 2 times.
  • Fix: This failure is similar to test_handles_empty_api_response. The test expects a single print call for the warning message, but two calls are observed (one notice, one warning). Adjust the mocks or test expectations to align with the single expected print call.

To resolve these, you'll need to carefully examine the test cases in .github/actions/bot-ci-failure/test_analyze_failure.py and adjust the mock configurations or the test logic to ensure print is called exactly once with the expected arguments in each failing test.

stktyagi added 2 commits May 3, 2026 17:40
Loosened print statement assertions to accomodate notices logging.
Changed ref to current branch for testing
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (/3).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/bot-ci-failure/test_analyze_failure.py:
- Around line 810-907: Add unit tests covering CI_RETRY_MODE="both": create two
tests similar to existing LLM tests that call main()—one where get_error_logs
returns a transient-only condition (transient_only=True) and assert that
open("transient_failure","w") was called and _should_retry_ci was never called
(use mock_retry.assert_not_called()), and another where transient_only=False and
mock_retry returns True and assert the marker file was created; reference the
functions/variables main(), _should_retry_ci, get_error_logs, CI_RETRY_MODE, and
ensure you patch analyze_failure._should_retry_ci and builtins.open as in the
other tests.

In @.github/workflows/reusable-bot-ci-failure.yml:
- Line 55: Change the hardcoded reusable workflow ref back to a stable branch
before merging: replace the string "ref: refactor/transient-rerun-mechanism"
with "ref: master" (or your repository's long-term default branch) in the
"Checkout Reusable Workflow" step so callers continue to resolve the ref after
this feature branch is deleted or diverges.
- Line 130: The workflow currently sets CI_RETRY_MODE to an empty string when
vars.CI_RETRY_MODE is unset, causing the Python logic that reads retry_mode (via
os.environ.get("CI_RETRY_MODE", "llm")) to treat it as "" and enable
transient-only mode; fix by providing a fallback in the workflow like using the
|| operator when setting CI_RETRY_MODE (so CI_RETRY_MODE: ${{ vars.CI_RETRY_MODE
|| 'llm' }}) or alternatively update the Python side to compute retry_mode as
(os.environ.get("CI_RETRY_MODE") or "llm").strip().lower() so an empty env var
won't override the intended default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c2db5d3-6c5f-42ed-83a0-b4c99bd34b82

📥 Commits

Reviewing files that changed from the base of the PR and between e73588c and 8307c64.

📒 Files selected for processing (2)
  • .github/actions/bot-ci-failure/test_analyze_failure.py
  • .github/workflows/reusable-bot-ci-failure.yml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Kilo Code Review
🔇 Additional comments (1)
.github/actions/bot-ci-failure/test_analyze_failure.py (1)

535-548: LGTM — TestParseRetryDecision covers the core cases.

The four assertions cover the two valid tokens, case/whitespace normalisation, and an unexpected output. The if not text: return None branch (empty string / None input) is not exercised, but those paths are trivial and the omission carries negligible risk.

Comment thread .github/actions/bot-ci-failure/test_analyze_failure.py
with:
repository: openwisp/openwisp-utils
ref: master
ref: refactor/transient-rerun-mechanism

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Revert ref to master before merging — feature branch will break callers post-merge.

ref: refactor/transient-rerun-mechanism is the development branch used for testing this change. Once this PR is merged and the branch is deleted or diverges, every repository that invokes this reusable workflow will fail at the "Checkout Reusable Workflow" step because the ref no longer resolves.

🛠 Proposed fix
-          ref: refactor/transient-rerun-mechanism
+          ref: master
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/reusable-bot-ci-failure.yml at line 55, Change the
hardcoded reusable workflow ref back to a stable branch before merging: replace
the string "ref: refactor/transient-rerun-mechanism" with "ref: master" (or your
repository's long-term default branch) in the "Checkout Reusable Workflow" step
so callers continue to resolve the ref after this feature branch is deleted or
diverges.

Comment thread .github/workflows/reusable-bot-ci-failure.yml
Forced default CI retry marker to llm

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good!

/3 issue

I have noticed again this:

Image

It should be (1/3). Can you please fix this as well here?

_parse_retry_decision crashes on whitespace-only output

analyze_failure.py:303-311. The guard if not text: return None catches
empty strings but not whitespace, because "\n" and " " are truthy.
The next line then does text.strip().splitlines()[0], which raises
IndexError: list index out of range on any whitespace-only input.

>>> _parse_retry_decision("\n")
IndexError
>>> _parse_retry_decision("   ")
IndexError
>>> _parse_retry_decision("   \n  ")
IndexError

The call site in _should_retry_ci (line 357) is outside the
try/except that wraps the API call, and main() does not wrap
_should_retry_ci either, so the exception propagates and crashes the
whole step. Gemini occasionally returns just a newline or empty token
under load, especially with max_output_tokens=5. With the default mode
now llm, this takes down the bot for every failure on that run.

Fix: change the early return to if not text or not text.strip() (or
guard the indexing with a length check), and add a test case for
whitespace-only input.

llm mode silently disables retry when the classifier API fails

analyze_failure.py:350-355 returns False on any classifier exception,
and analyze_failure.py:410-411 ignores transient_only entirely in
llm mode. Default mode is now llm (commit 83b1c05).

Concrete impact: when Gemini is rate-limited, returns 5xx, or replies
with an unparseable token, an obvious transient failure that the
deterministic heuristic already matches (selenium WebDriverException,
network unreachable, dependency install OSError, etc.) will not be
retried. This is a regression vs. the pre-PR behaviour, where the
heuristic was independent of Gemini availability. both mode does not
have this problem because the or short-circuits on transient_only.

Fix: in llm mode, fall back to transient_only when
_should_retry_ci returns False due to a classifier error. The
heuristic is free, and it is exactly the case where you want a
safety net.

CI_RETRY_MODE is undocumented

reusable-bot-ci-failure.yml:130 exposes the new variable but
docs/developer/reusable-github-utils.rst still describes only the
deterministic heuristic ("When the bot detects that all failures are
transient..."). Operators of other OpenWISP repos will not know:

  • the default is now llm, so enabling the bot on a new repo doubles
    Gemini calls per failed run with no opt-out documented;
  • the recognised values are llm, both, and any other value (the
    else branch at line 416, including unset, empty, or typo) selects
    heuristic-only;
  • in both mode the classifier call is skipped when the heuristic
    already matched.

The CI Failure Bot section needs the variable, its values, and the
default spelled out.

self.assertTrue(_parse_retry_decision(" yes \n"))

def test_unexpected_output(self):
self.assertIsNone(_parse_retry_decision("MAYBE"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

so what happens if the response is not YES or NO?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

in LLM mode that now falls back to the heuristic transient_only retry decision.

Comment thread .github/workflows/reusable-bot-ci-failure.yml
Comment thread .github/actions/bot-ci-failure/analyze_failure.py
To prevent unnecessary re-run, hardened the prompt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
.github/actions/bot-ci-failure/analyze_failure.py (1)

303-306: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard both first-line extractions against whitespace-only model output.

A classifier response like " \n" still crashes here: text.strip().splitlines()[0] in _parse_retry_decision and raw_text.strip().splitlines()[0] in the preview path both hit IndexError. That bypasses the intended “invalid output => no retry” behavior and can abort the action on a benign malformed response.

🐛 Proposed fix
 def _parse_retry_decision(text):
     if not text:
         return None
-    first = text.strip().splitlines()[0].strip().upper()
+    stripped = text.strip()
+    if not stripped:
+        return None
+    first = stripped.splitlines()[0].strip().upper()
     if first.startswith("YES"):
         return True
     if first.startswith("NO"):
         return False
     return None
@@
-    raw_text = response.text if response else ""
+    raw_text = response.text if response else ""
     decision = _parse_retry_decision(raw_text)
-    preview = raw_text.strip().splitlines()[0] if raw_text else "<empty>"
+    stripped_response = raw_text.strip() if raw_text else ""
+    preview = stripped_response.splitlines()[0] if stripped_response else "<empty>"

Also applies to: 356-358

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/bot-ci-failure/analyze_failure.py around lines 303 - 306,
The _parse_retry_decision function (and the preview-path code that uses
raw_text.strip().splitlines()[0]) can raise IndexError for whitespace-only model
outputs; update both to guard before indexing by computing stripped =
text.strip() (or raw_stripped = raw_text.strip()), return None if stripped is
empty, then extract the first line via stripped.splitlines()[0]. This ensures
whitespace-only responses are treated as invalid (no retry) without crashing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/bot-ci-failure/analyze_failure.py:
- Around line 323-330: The decision rule currently biases ambiguous cases toward
retry by instructing "If unsure, answer YES."; update the rule text in the
Decision rules block so the fallback is "If unsure, answer NO." and clarify that
YES is only for concrete transient indicators (network/DNS/service outages,
dependency download errors, browser crashes, flaky UI/selenium/webdriver issues)
to avoid unnecessary auto-reruns. Ensure the exact phrasing replacing "If
unsure, answer YES." is "If unsure, answer NO." and add a brief note that YES
should be reserved for logs with concrete transient signals.

---

Duplicate comments:
In @.github/actions/bot-ci-failure/analyze_failure.py:
- Around line 303-306: The _parse_retry_decision function (and the preview-path
code that uses raw_text.strip().splitlines()[0]) can raise IndexError for
whitespace-only model outputs; update both to guard before indexing by computing
stripped = text.strip() (or raw_stripped = raw_text.strip()), return None if
stripped is empty, then extract the first line via stripped.splitlines()[0].
This ensures whitespace-only responses are treated as invalid (no retry) without
crashing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a71dfe88-b655-41fc-a94e-b0b0f98d7ba7

📥 Commits

Reviewing files that changed from the base of the PR and between 8307c64 and 3bc1e23.

📒 Files selected for processing (1)
  • .github/actions/bot-ci-failure/analyze_failure.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
  • GitHub Check: Kilo Code Review
🔇 Additional comments (1)
.github/actions/bot-ci-failure/analyze_failure.py (1)

406-425: Mode selection and marker write path look good.

Computing should_retry once and only writing transient_failure behind that gate keeps the workflow contract explicit and avoids mutating the marker on non-retry outcomes.

Comment thread .github/actions/bot-ci-failure/analyze_failure.py

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @stktyagi 👏

@nemesifier nemesifier changed the title [ci] Refactor transient error re-run mechanism [ci] Added LLM based transient error re-run mechanism May 4, 2026
@nemesifier
nemesifier merged commit 40561a9 into master May 4, 2026
39 checks passed
@nemesifier
nemesifier deleted the refactor/transient-rerun-mechanism branch May 4, 2026 16:42
@coderabbitai coderabbitai Bot mentioned this pull request May 4, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement github_actions Pull requests that update GitHub Actions code helper-bots Helper bots, release management automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants