Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[run]
source = openwisp_utils
parallel = true
concurrency = multiprocessing
source =
openwisp_utils
branch = True
parallel = True

[report]
omit =
/*/test*
/*/__init__.py
/*/migrations/*
*/migrations/*
*/__init__.py
43 changes: 43 additions & 0 deletions .github/workflows/ci-failure-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
name: CI Failure Bot

on:
workflow_run:
workflows: ["OpenWISP Utils CI Build"]
types:
- completed

permissions:
issues: write
pull-requests: write
contents: read

jobs:
ci-failure-bot:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' && !contains(github.event.workflow_run.actor.login, 'dependabot') }}

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install dependencies
run: |
pip install -e .[github_actions]

- name: Run CI Failure Bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number || '' }}
run: |
echo "Starting CI Failure Bot..."
python -m openwisp_utils.bots.ci_failure.bot
Comment on lines +20 to +42
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

git diff will always be empty: workflow_run checks out the default branch, not the PR branch.

The workflow_run event triggers on the repository's default branch. Line 22 checks out the default branch, and then the bot runs git diff origin/{default_branch} — comparing the default branch against itself, which always yields an empty diff. get_pr_diff() will always return None, so Gemini never receives PR diff context.

To get the actual PR diff, either:

  1. Fetch the diff via the GitHub API (e.g., requests.get(pr.diff_url, ...)) instead of relying on local git, or
  2. Checkout the PR's head ref before running the bot:
Option 2: Checkout PR head
       - name: Checkout repository
         uses: actions/checkout@v6
+        with:
+          fetch-depth: 0
+
+      - name: Fetch PR branch
+        if: ${{ github.event.workflow_run.pull_requests[0].number }}
+        run: |
+          PR_HEAD=${{ github.event.workflow_run.head_sha }}
+          git fetch origin "$PR_HEAD"
+          git checkout "$PR_HEAD"

Option 1 (API-based diff) is more robust for workflow_run events and was previously suggested.

🤖 Prompt for AI Agents
In @.github/workflows/ci-failure-bot.yml around lines 20 - 42, The workflow_run
checkout yields the default branch so local git diff is empty; update
get_pr_diff() in openwisp_utils.bots.ci_failure.bot to fall back to retrieving
the PR diff via the GitHub API (use PR number from env PR_NUMBER and
GITHUB_TOKEN, call the PR's diff_url with the appropriate Accept header and
return the diff text) instead of relying solely on local git, ensuring the bot
supplies the real PR diff to Gemini when running under the workflow_run event.

echo "CI Failure Bot completed successfully"
Comment on lines +40 to +43
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 | 🟡 Minor

Success message prints regardless of bot exit status.

With set +e on line 41, the script continues even if the Python command fails, so line 44 will always print "CI Failure Bot completed successfully" even when the bot encounters errors.

🔧 Suggested fix to conditionally print success
       run: |
         set +e  # Allow bot to handle errors internally
         echo "Starting CI Failure Bot..."
         python -m openwisp_utils.bots.ci_failure.bot
-        echo "CI Failure Bot completed successfully"
+        exit_code=$?
+        if [ $exit_code -eq 0 ]; then
+          echo "CI Failure Bot completed successfully"
+        else
+          echo "CI Failure Bot exited with code $exit_code"
+        fi
🤖 Prompt for AI Agents
In @.github/workflows/ci-failure-bot.yml around lines 40 - 44, The script always
prints the success message because it uses set +e then unconditionally echoes
"CI Failure Bot completed successfully"; change it to capture and check the exit
status of the python invocation (the `python -m
openwisp_utils.bots.ci_failure.bot` command) and only print the success message
when that command exits zero, otherwise print an error and exit with the same
non-zero status so the CI job reflects the failure (reference the `set +e`, the
`python -m openwisp_utils.bots.ci_failure.bot` invocation, and the `echo "CI
Failure Bot completed successfully"` line).

2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
run: |
pip install -U pip wheel setuptools
pip install -U -r requirements-test.txt
pip install -e .[qa,rest,selenium,releaser]
pip install -e .[qa,rest,selenium,releaser,github_actions]
pip install ${{ matrix.django-version }}
sudo npm install -g prettier

Expand Down
127 changes: 127 additions & 0 deletions docs/developer/reusable-github-utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,133 @@ times with a 30 second delay between attempts.
attempts, the action will exit with a non-zero status, causing the
workflow to fail.

CI Failure Bot
~~~~~~~~~~~~~~

This GitHub workflow analyzes failed CI builds when a pull request context
is available and provides intelligent feedback to contributors using
AI-powered analysis.

The bot examines build logs, PR changes, and workflow context to generate
specific, actionable guidance that helps contributors fix issues quickly.

**Inputs**

- ``GEMINI_API_KEY`` (optional): Google Gemini API key for AI analysis. If
not provided, the bot uses fallback responses
- ``GEMINI_MODEL`` (optional): Gemini model to use. Defaults to
``gemini-2.5-flash``

**Usage Example**

You can use this workflow in your repository as follows:

.. code-block:: yaml

name: CI Failure Bot

on:
workflow_run:
workflows: ["OpenWISP Utils CI Build"]
types:
- completed

permissions:
issues: write
pull-requests: write
contents: read

jobs:
ci-failure-bot:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' && !contains(github.event.workflow_run.actor.login, 'dependabot') }}

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install dependencies
run: |
pip install -e .[github_actions]

- name: Run CI Failure Bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number || '' }}
run: python -m openwisp_utils.bots.ci_failure.bot

This example automatically triggers when the "OpenWISP Utils CI Build"
workflow fails, analyzes the failure using Gemini AI, and posts
intelligent feedback to the associated pull request.

**Features**

- **Automatic triggering**: Responds to CI build failures in pull requests
- **AI-powered analysis**: Uses Google Gemini to analyze failure logs and
provide specific guidance
- **Targeted remediation**: Suggests QA commands, test commands, or setup
fixes depending on which checks failed (no generic advice)
- **Intelligent responses**: Provides direct, actionable feedback based on
actual failure context
- **Comment deduplication**: Updates existing comments instead of creating
duplicates
- **Dependabot exclusion**: Automatically skips dependency update PRs
- **Fork detection**: Skips external PRs for security
- **Fallback handling**: Provides basic guidance if AI analysis fails

**Configuration**
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.

the formatting of this line doesn't make sense as this is bold text and in the next line there's a heading, please use a restructuredtext visualizer to preview this page so you can see the result and edit weird details.

+++++++++++++++++

Repository Secrets
++++++++++++++++++

The following secrets can be configured in the repository for enhanced
functionality:

- ``GEMINI_API_KEY``: Google Gemini API key for AI analysis (optional -
fallback responses used if not provided)

Environment Variables
+++++++++++++++++++++

Optional environment variables for customization:

- ``GEMINI_MODEL``: Gemini model to use (default: ``gemini-2.5-flash``)
Comment on lines +141 to +158
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 | 🟡 Minor

RST heading hierarchy: "Configuration", "Repository Secrets", and "Environment Variables" are siblings, not parent-child.

All three headings use +++ underlines, making them the same level in RST. "Repository Secrets" and "Environment Variables" should be sub-sections of "Configuration" and need a different underline character (e.g., ^^^^^).

📝 Suggested fix
 **Configuration**
 +++++++++++++++++
 
 Repository Secrets
-++++++++++++++++++
+^^^^^^^^^^^^^^^^^^
 
 The following secrets can be configured in the repository for enhanced
 functionality:
 
 - ``GEMINI_API_KEY``: Google Gemini API key for AI analysis (optional -
   fallback responses used if not provided)
 
 Environment Variables
-+++++++++++++++++++++
+^^^^^^^^^^^^^^^^^^^^^
 
 Optional environment variables for customization:
🤖 Prompt for AI Agents
In `@docs/developer/reusable-github-utils.rst` around lines 141 - 158, Change the
RST heading hierarchy so "Repository Secrets" and "Environment Variables" are
subsections of "Configuration": update the underline adornments for the
"Repository Secrets" and "Environment Variables" headings (currently using +++)
to a different character (e.g., ^^^^^) to make them lower-level headings under
the "Configuration" section; keep the "Configuration" heading as-is so
"Repository Secrets" and "Environment Variables" become its child subsections.


**Limitations**

- **Pull request context availability**: When triggered via
``workflow_run``, GitHub may not always provide an associated pull
request (for example, when builds are triggered by pushes or scheduled
workflows). In these cases, the bot will not post a comment, as no pull
request context is available.
- **Optional Gemini API**: Google Gemini API access enhances analysis
quality, but the bot provides fallback responses when unavailable
- **Privacy consideration**: PR diffs and build logs are sent to Google's
Gemini AI service for analysis when API key is provided. Organizations
with sensitive codebases should review Google's data handling policies
- **API costs**: Each CI failure with Gemini enabled triggers an API call.
Monitor usage to manage costs, especially in repositories with frequent
CI failures
- Analysis quality depends on error log clarity
- May not handle very complex or unusual failure scenarios
- Skips dependabot PRs to avoid unnecessary noise

.. note::

If the Gemini API is unavailable or analysis fails, the bot provides a
fallback response with standard OpenWISP QA guidance. Critical errors
are logged in GitHub Actions, but the workflow is designed to complete
safely without blocking contributor feedback.

GitHub Workflows
----------------

Expand Down
1 change: 1 addition & 0 deletions openwisp_utils/bots/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "openwisp_utils.bots.apps.BotsConfig"
7 changes: 7 additions & 0 deletions openwisp_utils/bots/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class BotsConfig(AppConfig):
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.

What are you doing? This is totally out of place! This is not a django app!

name = "openwisp_utils.bots"
label = "openwisp_utils_bots"
verbose_name = "OpenWISP Bots"
1 change: 1 addition & 0 deletions openwisp_utils/bots/ci_failure/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# CI Failure Bot
Loading
Loading