mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-21 04:50:39 +08:00
Compare commits
13 Commits
bec70dc96a
...
9773afe155
Author | SHA1 | Date | |
---|---|---|---|
9773afe155 | |||
0a8a263809 | |||
597f553dd5 | |||
4b6fcfe60e | |||
7cc4206b70 | |||
8906a81a2e | |||
6179eeca58 | |||
e8c73e7baa | |||
754d47f187 | |||
c4dd07b3b8 | |||
3aaa727e05 | |||
6108f96bff | |||
5a00897cbe |
@ -51,6 +51,430 @@ When you open your next PR, you should see a comment from `github-actions` bot w
|
||||
|
||||
See detailed usage instructions in the [USAGE GUIDE](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-action)
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
This section provides detailed, step-by-step examples for configuring PR-Agent with different models and advanced options in GitHub Actions.
|
||||
|
||||
### Quick Start Examples
|
||||
|
||||
#### Basic Setup (OpenAI Default)
|
||||
|
||||
Copy this minimal workflow to get started with the default OpenAI models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
#### Gemini Setup
|
||||
|
||||
Ready-to-use workflow for Gemini models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent (Gemini)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
config.model: "gemini/gemini-1.5-flash"
|
||||
config.fallback_models: '["gemini/gemini-1.5-flash"]'
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Claude Setup
|
||||
|
||||
Ready-to-use workflow for Claude models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent (Claude)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
config.model: "anthropic/claude-3-opus-20240229"
|
||||
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Basic Configuration with Tool Controls
|
||||
|
||||
Start with this enhanced workflow that includes tool configuration:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Enable/disable automatic tools
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
# Configure which PR events trigger the action
|
||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
||||
```
|
||||
|
||||
### Switching Models
|
||||
|
||||
#### Using Gemini (Google AI Studio)
|
||||
|
||||
To use Gemini models instead of the default OpenAI models:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to Gemini
|
||||
config.model: "gemini/gemini-1.5-flash"
|
||||
config.fallback_models: '["gemini/gemini-1.5-flash"]'
|
||||
# Add your Gemini API key
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- Add `GEMINI_API_KEY` to your repository secrets (get it from [Google AI Studio](https://aistudio.google.com/))
|
||||
|
||||
**Note:** When using non-OpenAI models like Gemini, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
|
||||
|
||||
#### Using Claude (Anthropic)
|
||||
|
||||
To use Claude models:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to Claude
|
||||
config.model: "anthropic/claude-3-opus-20240229"
|
||||
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
|
||||
# Add your Anthropic API key
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- Add `ANTHROPIC_KEY` to your repository secrets (get it from [Anthropic Console](https://console.anthropic.com/))
|
||||
|
||||
**Note:** When using non-OpenAI models like Claude, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
|
||||
|
||||
#### Using Azure OpenAI
|
||||
|
||||
To use Azure OpenAI services:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Azure OpenAI configuration
|
||||
OPENAI.API_TYPE: "azure"
|
||||
OPENAI.API_VERSION: "2023-05-15"
|
||||
OPENAI.API_BASE: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
|
||||
OPENAI.DEPLOYMENT_ID: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
|
||||
# Set the model to match your Azure deployment
|
||||
config.model: "gpt-4o"
|
||||
config.fallback_models: '["gpt-4o"]'
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- `AZURE_OPENAI_KEY`: Your Azure OpenAI API key
|
||||
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL
|
||||
- `AZURE_OPENAI_DEPLOYMENT`: Your deployment name
|
||||
|
||||
#### Using Local Models (Ollama)
|
||||
|
||||
To use local models via Ollama:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to a local Ollama model
|
||||
config.model: "ollama/qwen2.5-coder:32b"
|
||||
config.fallback_models: '["ollama/qwen2.5-coder:32b"]'
|
||||
config.custom_model_max_tokens: "128000"
|
||||
# Ollama configuration
|
||||
OLLAMA.API_BASE: "http://localhost:11434"
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Note:** For local models, you'll need to use a self-hosted runner with Ollama installed, as GitHub Actions hosted runners cannot access localhost services.
|
||||
|
||||
### Advanced Configuration Options
|
||||
|
||||
#### Custom Review Instructions
|
||||
|
||||
Add specific instructions for the review process:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Custom review instructions
|
||||
pr_reviewer.extra_instructions: "Focus on security vulnerabilities and performance issues. Check for proper error handling."
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Language-Specific Configuration
|
||||
|
||||
Configure for specific programming languages:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Language-specific settings
|
||||
pr_reviewer.extra_instructions: "Focus on Python best practices, type hints, and docstrings."
|
||||
pr_code_suggestions.num_code_suggestions: "8"
|
||||
pr_code_suggestions.suggestions_score_threshold: "7"
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Selective Tool Execution
|
||||
|
||||
Run only specific tools automatically:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Only run review and describe, skip improve
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "false"
|
||||
# Only trigger on PR open and reopen
|
||||
github_action_config.pr_actions: '["opened", "reopened"]'
|
||||
```
|
||||
|
||||
### Using Configuration Files
|
||||
|
||||
Instead of setting all options via environment variables, you can use a `.pr_agent.toml` file in your repository root:
|
||||
|
||||
1. Create a `.pr_agent.toml` file in your repository root:
|
||||
|
||||
```toml
|
||||
[config]
|
||||
model = "gemini/gemini-1.5-flash"
|
||||
fallback_models = ["anthropic/claude-3-opus-20240229"]
|
||||
|
||||
[pr_reviewer]
|
||||
extra_instructions = "Focus on security issues and code quality."
|
||||
|
||||
[pr_code_suggestions]
|
||||
num_code_suggestions = 6
|
||||
suggestions_score_threshold = 7
|
||||
```
|
||||
|
||||
2. Use a simpler workflow file:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Troubleshooting Common Issues
|
||||
|
||||
#### Model Not Found Errors
|
||||
|
||||
If you get model not found errors:
|
||||
|
||||
1. **Check model name format**: Ensure you're using the correct model identifier format (e.g., `gemini/gemini-1.5-flash`, not just `gemini-1.5-flash`)
|
||||
|
||||
2. **Verify API keys**: Make sure your API keys are correctly set as repository secrets
|
||||
|
||||
3. **Check model availability**: Some models may not be available in all regions or may require specific access
|
||||
|
||||
#### Environment Variable Format
|
||||
|
||||
Remember these key points about environment variables:
|
||||
|
||||
- Use dots (`.`) or double underscores (`__`) to separate sections and keys
|
||||
- Boolean values should be strings: `"true"` or `"false"`
|
||||
- Arrays should be JSON strings: `'["item1", "item2"]'`
|
||||
- Model names are case-sensitive
|
||||
|
||||
#### Rate Limiting
|
||||
|
||||
If you encounter rate limiting:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Add fallback models for better reliability
|
||||
config.fallback_models: '["gpt-4o", "gpt-3.5-turbo"]'
|
||||
# Increase timeout for slower models
|
||||
config.ai_timeout: "300"
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Common Error Messages and Solutions
|
||||
|
||||
**Error: "Model not found"**
|
||||
- **Solution**: Check the model name format and ensure it matches the exact identifier. See the [Changing a model in PR-Agent](../usage-guide/changing_a_model.md) guide for supported models and their correct identifiers.
|
||||
|
||||
**Error: "API key not found"**
|
||||
- **Solution**: Verify that your API key is correctly set as a repository secret and the environment variable name matches exactly
|
||||
- **Note**: For non-OpenAI models (Gemini, Claude, etc.), you only need the model-specific API key, not `OPENAI_KEY`
|
||||
|
||||
**Error: "Rate limit exceeded"**
|
||||
- **Solution**: Add fallback models or increase the `config.ai_timeout` value
|
||||
|
||||
**Error: "Permission denied"**
|
||||
- **Solution**: Ensure your workflow has the correct permissions set:
|
||||
```yaml
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
```
|
||||
|
||||
**Error: "Invalid JSON format"**
|
||||
- **Solution**: Check that arrays are properly formatted as JSON strings:
|
||||
```yaml
|
||||
# Correct
|
||||
config.fallback_models: '["model1", "model2"]'
|
||||
# Incorrect (interpreted as a YAML list, not a string)
|
||||
config.fallback_models: ["model1", "model2"]
|
||||
```
|
||||
|
||||
#### Debugging Tips
|
||||
|
||||
1. **Enable verbose logging**: Add `config.verbosity_level: "2"` to see detailed logs
|
||||
2. **Check GitHub Actions logs**: Look at the step output for specific error messages
|
||||
3. **Test with minimal configuration**: Start with just the basic setup and add options one by one
|
||||
4. **Verify secrets**: Double-check that all required secrets are set in your repository settings
|
||||
|
||||
#### Performance Optimization
|
||||
|
||||
For better performance with large repositories:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Optimize for large PRs
|
||||
config.large_patch_policy: "clip"
|
||||
config.max_model_tokens: "32000"
|
||||
config.patch_extra_lines_before: "3"
|
||||
config.patch_extra_lines_after: "1"
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Reference
|
||||
|
||||
For more detailed configuration options, see:
|
||||
- [Changing a model in PR-Agent](../usage-guide/changing_a_model.md)
|
||||
- [Configuration options](../usage-guide/configuration_options.md)
|
||||
- [Automations and usage](../usage-guide/automations_and_usage.md#github-action)
|
||||
|
||||
### Using a specific release
|
||||
|
||||
!!! tip ""
|
||||
@ -296,4 +720,4 @@ After you set up AWS CodeCommit using the instructions above, here is an example
|
||||
PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
||||
--pr_url https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/MY_REPO_NAME/pull-requests/321 \
|
||||
review
|
||||
```
|
||||
```
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The `compliance` tool performs comprehensive compliance checks on PR code changes, validating them against security standards, ticket requirements, and custom organizational compliance checklists, thereby helping teams maintain consistent code quality and security practices while ensuring that development work aligns with business requirements.
|
||||
The `compliance` tool performs comprehensive compliance checks on PR code changes, validating them against security standards, ticket requirements, and custom organizational compliance checklists, thereby helping teams, enterprises, and agents maintain consistent code quality and security practices while ensuring that development work aligns with business requirements.
|
||||
|
||||
=== "Fully Compliant"
|
||||
{width=256}
|
||||
@ -67,7 +67,7 @@ Scans for security vulnerabilities and potential exploits in the PR code changes
|
||||
|
||||
- **Verified Security Concerns** 🔴: Clear security vulnerabilities that require immediate attention
|
||||
- **Possible Security Risks** ⚪: Potential security issues that need human verification
|
||||
- **No Security Concerns** 🟢: No security vulnerabilities were detected
|
||||
- **No Security Concerns** 🟢: No security vulnerabilities detected
|
||||
|
||||
Examples of security issues:
|
||||
|
||||
@ -103,7 +103,7 @@ Validates against an organization-specific compliance checklist:
|
||||
|
||||
### Setting Up Custom Compliance
|
||||
|
||||
Each compliance is defined in a YAML file, as follows:
|
||||
Each compliance is defined in a YAML file as follows:
|
||||
- `title`: Used to provide a clear name for the compliance
|
||||
- `compliance_label`: Used to automatically generate labels for non-compliance issues
|
||||
- `objective`, `success_criteria`, and `failure_criteria`: These fields are used to clearly define what constitutes compliance
|
||||
@ -122,7 +122,7 @@ Each compliance is defined in a YAML file, as follows:
|
||||
...
|
||||
```
|
||||
|
||||
???+ tip "Writing effective compliance checklist"
|
||||
???+ tip "Writing effective compliance checklists"
|
||||
- Avoid overly complex or subjective compliances that are hard to verify
|
||||
- Keep compliances focused on security, business requirements, and critical standards
|
||||
- Use clear, actionable language that developers can understand
|
||||
@ -135,7 +135,7 @@ Qodo Merge supports hierarchical compliance checklists using a dedicated global
|
||||
|
||||
#### Setting up global hierarchical compliance
|
||||
|
||||
1\. Create a new repository named `pr-agent-settings` in your organization/workspace.
|
||||
1\. Create a new repository named `pr-agent-settings` in your organization or workspace.
|
||||
|
||||
2\. Build the folder hierarchy in your `pr-agent-settings` repository:
|
||||
|
||||
@ -153,14 +153,14 @@ pr-agent-settings/
|
||||
├── qodo-merge/ # For standalone repositories
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
└── qodo-monorepo/ # For monorepo-specific compliance
|
||||
├── pr_compliance_checklist.yaml # Root level monorepo compliance
|
||||
├── pr_compliance_checklist.yaml # Root-level monorepo compliance
|
||||
├── qodo-github/ # Subproject compliance
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
└── qodo-gitlab/ # Another subproject
|
||||
└── pr_compliance_checklist.yaml
|
||||
```
|
||||
|
||||
3\. Define the metadata file `metadata.yaml` in the `pr-agent-settings` root:
|
||||
3\. Define the metadata file `metadata.yaml` in the root of `pr-agent-settings`:
|
||||
|
||||
```yaml
|
||||
# Standalone repos
|
||||
@ -225,7 +225,7 @@ enable_global_pr_compliance = true
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>extra_instructions</b></td>
|
||||
<td>Optional extra instructions for the tool. For example: "focus on the changes in the file X. Ignore changes in ...". Default is empty string.</td>
|
||||
<td>Optional extra instructions for the tool. For example: "Ensure that all error-handling paths in the code contain appropriate logging statements". Default is empty string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>persistent_comment</b></td>
|
||||
@ -233,11 +233,11 @@ enable_global_pr_compliance = true
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_user_defined_compliance_labels</b></td>
|
||||
<td>If set to true, the tool will add labels for custom compliance violations. Default is true.</td>
|
||||
<td>If set to true, the tool will add the label `Failed compliance check` for custom compliance violations. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_estimate_effort_to_review</b></td>
|
||||
<td>If set to true, the tool will estimate the effort required to review the PR (1-5 scale). Default is true.</td>
|
||||
<td>If set to true, the tool will estimate the effort required to review the PR (1-5 scale) as a label. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_todo_scan</b></td>
|
||||
@ -245,11 +245,11 @@ enable_global_pr_compliance = true
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_update_pr_compliance_checkbox</b></td>
|
||||
<td>If set to true, the tool will add an update checkbox to refresh compliance status. Default is true.</td>
|
||||
<td>If set to true, the tool will add an update checkbox to refresh compliance status following push events. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_help_text</b></td>
|
||||
<td>If set to true, the tool will display a help text in the comment. Default is false.</td>
|
||||
<td>If set to true, the tool will display help text in the comment. Default is false.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -262,7 +262,7 @@ enable_global_pr_compliance = true
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_compliance_labels_security</b></td>
|
||||
<td>If set to true, the tool will add security-related labels to the PR. Default is true.</td>
|
||||
<td>If set to true, the tool will add a `Possible security concern` label to the PR when security-related concerns are detected. Default is true.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -124,6 +124,10 @@ This option is enabled by default via the `pr_description.enable_pr_diagram` par
|
||||
<td><b>enable_semantic_files_types</b></td>
|
||||
<td>If set to true, "Changes walkthrough" section will be generated. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>file_table_collapsible_open_by_default</b></td>
|
||||
<td>If set to true, the file list in the "Changes walkthrough" section will be open by default. If set to false, it will be closed by default. Default is false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>collapsible_file_list</b></td>
|
||||
<td>If set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".</td>
|
||||
|
@ -202,6 +202,25 @@ publish_labels = false
|
||||
|
||||
to prevent Qodo Merge from publishing labels when running the `describe` tool.
|
||||
|
||||
#### Quick Reference: Model Configuration in GitHub Actions
|
||||
|
||||
For detailed step-by-step examples of configuring different models (Gemini, Claude, Azure OpenAI, etc.) in GitHub Actions, see the [Configuration Examples](../installation/github.md#configuration-examples) section in the installation guide.
|
||||
|
||||
**Common Model Configuration Patterns:**
|
||||
|
||||
- **OpenAI**: Set `config.model: "gpt-4o"` and `OPENAI_KEY`
|
||||
- **Gemini**: Set `config.model: "gemini/gemini-1.5-flash"` and `GOOGLE_AI_STUDIO.GEMINI_API_KEY` (no `OPENAI_KEY` needed)
|
||||
- **Claude**: Set `config.model: "anthropic/claude-3-opus-20240229"` and `ANTHROPIC.KEY` (no `OPENAI_KEY` needed)
|
||||
- **Azure OpenAI**: Set `OPENAI.API_TYPE: "azure"`, `OPENAI.API_BASE`, and `OPENAI.DEPLOYMENT_ID`
|
||||
- **Local Models**: Set `config.model: "ollama/model-name"` and `OLLAMA.API_BASE`
|
||||
|
||||
**Environment Variable Format:**
|
||||
- Use dots (`.`) to separate sections and keys: `config.model`, `pr_reviewer.extra_instructions`
|
||||
- Boolean values as strings: `"true"` or `"false"`
|
||||
- Arrays as JSON strings: `'["item1", "item2"]'`
|
||||
|
||||
For complete model configuration details, see [Changing a model in PR-Agent](changing_a_model.md).
|
||||
|
||||
### GitLab Webhook
|
||||
|
||||
After setting up a GitLab webhook, to control which commands will run automatically when a new MR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
|
||||
|
@ -3,5 +3,5 @@
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-M6PJSFV');</script>
|
||||
})(window,document,'script','dataLayer','GTM-5C9KZBM3');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
|
@ -70,7 +70,8 @@ class ReasoningEffort(str, Enum):
|
||||
|
||||
|
||||
class PRDescriptionHeader(str, Enum):
|
||||
CHANGES_WALKTHROUGH = "### **Changes walkthrough** 📝"
|
||||
DIAGRAM_WALKTHROUGH = "Diagram Walkthrough"
|
||||
FILE_WALKTHROUGH = "File Walkthrough"
|
||||
|
||||
|
||||
def get_setting(key: str) -> Any:
|
||||
@ -1284,14 +1285,35 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
if not description_full:
|
||||
return "", []
|
||||
|
||||
description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH.value)
|
||||
base_description_str = description_split[0]
|
||||
changes_walkthrough_str = ""
|
||||
files = []
|
||||
if len(description_split) > 1:
|
||||
changes_walkthrough_str = description_split[1]
|
||||
# description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value)
|
||||
if PRDescriptionHeader.FILE_WALKTHROUGH.value in description_full:
|
||||
try:
|
||||
# FILE_WALKTHROUGH are presented in a collapsible section in the description
|
||||
regex_pattern = r'<details.*?>\s*<summary>\s*<h3>\s*' + re.escape(PRDescriptionHeader.FILE_WALKTHROUGH.value) + r'\s*</h3>\s*</summary>'
|
||||
description_split = re.split(regex_pattern, description_full, maxsplit=1, flags=re.DOTALL)
|
||||
|
||||
# If the regex pattern is not found, fallback to the previous method
|
||||
if len(description_split) == 1:
|
||||
get_logger().debug("Could not find regex pattern for file walkthrough, falling back to simple split")
|
||||
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
|
||||
except Exception as e:
|
||||
get_logger().warning(f"Failed to split description using regex, falling back to simple split: {e}")
|
||||
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
|
||||
|
||||
if len(description_split) < 2:
|
||||
get_logger().error("Failed to split description into base and changes walkthrough", artifact={'description': description_full})
|
||||
return description_full.strip(), []
|
||||
|
||||
base_description_str = description_split[0].strip()
|
||||
changes_walkthrough_str = ""
|
||||
files = []
|
||||
if len(description_split) > 1:
|
||||
changes_walkthrough_str = description_split[1]
|
||||
else:
|
||||
get_logger().debug("No changes walkthrough found")
|
||||
else:
|
||||
get_logger().debug("No changes walkthrough found")
|
||||
base_description_str = description_full.strip()
|
||||
return base_description_str, []
|
||||
|
||||
try:
|
||||
if changes_walkthrough_str:
|
||||
@ -1314,18 +1336,20 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
try:
|
||||
if isinstance(file_data, tuple):
|
||||
file_data = file_data[0]
|
||||
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*<li>(.*?)</details>'
|
||||
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*(?:<li>|•)(.*?)</details>'
|
||||
res = re.search(pattern, file_data, re.DOTALL)
|
||||
if not res or res.lastindex != 4:
|
||||
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong><dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\n\n\s*(.*?)</details>'
|
||||
res = re.search(pattern_back, file_data, re.DOTALL)
|
||||
if not res or res.lastindex != 4:
|
||||
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*-\s*(.*?)\s*</details>' # looking for hyphen ('- ')
|
||||
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*-\s*(.*?)\s*</details>' # looking for hypen ('- ')
|
||||
res = re.search(pattern_back, file_data, re.DOTALL)
|
||||
if res and res.lastindex == 4:
|
||||
short_filename = res.group(1).strip()
|
||||
short_summary = res.group(2).strip()
|
||||
long_filename = res.group(3).strip()
|
||||
if long_filename.endswith('<ul>'):
|
||||
long_filename = long_filename[:-4].strip()
|
||||
long_summary = res.group(4).strip()
|
||||
long_summary = long_summary.replace('<br> *', '\n*').replace('<br>','').replace('\n','<br>')
|
||||
long_summary = h.handle(long_summary).strip()
|
||||
@ -1344,7 +1368,7 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
if '<code>...</code>' in file_data:
|
||||
pass # PR with many files. some did not get analyzed
|
||||
else:
|
||||
get_logger().error(f"Failed to parse description", artifact={'description': file_data})
|
||||
get_logger().warning(f"Failed to parse description", artifact={'description': file_data})
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to process description: {e}", artifact={'description': file_data})
|
||||
|
||||
|
@ -380,7 +380,7 @@ class AzureDevopsProvider(GitProvider):
|
||||
pr_body = pr_body[:ind]
|
||||
|
||||
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
|
||||
changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH.value
|
||||
changes_walkthrough_text = PRDescriptionHeader.FILE_WALKTHROUGH.value
|
||||
ind = pr_body.find(changes_walkthrough_text)
|
||||
if ind != -1:
|
||||
pr_body = pr_body[:ind]
|
||||
|
@ -106,7 +106,7 @@ extra_instructions = ""
|
||||
enable_pr_type=true
|
||||
final_update_message = true
|
||||
enable_help_text=false
|
||||
enable_help_comment=true
|
||||
enable_help_comment=false
|
||||
enable_pr_diagram=true # adds a section with a diagram of the PR changes
|
||||
# describe as comment
|
||||
publish_description_as_comment=false
|
||||
|
@ -48,8 +48,8 @@ class PRDescription(BaseModel):
|
||||
description: str = Field(description="summarize the PR changes in up to four bullet points, each up to 8 words. For large PRs, add sub-bullets if needed. Order bullets by importance, with each bullet highlighting a key change group.")
|
||||
title: str = Field(description="a concise and descriptive title that captures the PR's main theme")
|
||||
{%- if enable_pr_diagram %}
|
||||
changes_diagram: str = Field(description="a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID[\"node description\"]. (2) Then define the links: nodeID1 -- \"link text\" --> nodeID2. Node description must always be surrounded with quotation marks.")
|
||||
{%- endif %}
|
||||
changes_diagram: str = Field(description='a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID["node description"]. (2) Then define the links: nodeID1 -- "link text" --> nodeID2. Node description must always be surrounded with double quotation marks')
|
||||
'{%- endif %}
|
||||
{%- if enable_semantic_files_types %}
|
||||
pr_files: List[FileDescription] = Field(max_items=20, description="a list of all the files that were changed in the PR, and summary of their changes. Each file must be analyzed regardless of change size.")
|
||||
{%- endif %}
|
||||
@ -67,11 +67,11 @@ description: |
|
||||
title: |
|
||||
...
|
||||
{%- if enable_pr_diagram %}
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
{%- endif %}
|
||||
{%- if enable_semantic_files_types %}
|
||||
pr_files:
|
||||
@ -155,11 +155,11 @@ description: |
|
||||
title: |
|
||||
...
|
||||
{%- if enable_pr_diagram %}
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
{%- endif %}
|
||||
{%- if enable_semantic_files_types %}
|
||||
pr_files:
|
||||
|
@ -128,7 +128,7 @@ class PRDescription:
|
||||
pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer()
|
||||
if not self.git_provider.is_supported(
|
||||
"publish_file_comments") or not get_settings().pr_description.inline_file_summary:
|
||||
pr_body += "\n\n" + changes_walkthrough
|
||||
pr_body += "\n\n" + changes_walkthrough + "___\n\n"
|
||||
get_logger().debug("PR output", artifact={"title": pr_title, "body": pr_body})
|
||||
|
||||
# Add help text if gfm_markdown is supported
|
||||
@ -331,7 +331,8 @@ class PRDescription:
|
||||
else:
|
||||
original_prediction_dict = original_prediction_loaded
|
||||
if original_prediction_dict:
|
||||
filenames_predicted = [file.get('filename', '').strip() for file in original_prediction_dict.get('pr_files', [])]
|
||||
files = original_prediction_dict.get('pr_files', [])
|
||||
filenames_predicted = [file.get('filename', '').strip() for file in files if isinstance(file, dict)]
|
||||
else:
|
||||
filenames_predicted = []
|
||||
|
||||
@ -555,15 +556,11 @@ class PRDescription:
|
||||
"""
|
||||
|
||||
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
|
||||
markdown_text = ""
|
||||
# Don't display 'PR Labels'
|
||||
if 'labels' in self.data and self.git_provider.is_supported("get_labels"):
|
||||
self.data.pop('labels')
|
||||
if not get_settings().pr_description.enable_pr_type:
|
||||
self.data.pop('type')
|
||||
for key, value in self.data.items():
|
||||
markdown_text += f"## **{key}**\n\n"
|
||||
markdown_text += f"{value}\n\n"
|
||||
|
||||
# Remove the 'PR Title' key from the dictionary
|
||||
ai_title = self.data.pop('title', self.vars["title"])
|
||||
@ -579,6 +576,10 @@ class PRDescription:
|
||||
pr_body, changes_walkthrough = "", ""
|
||||
pr_file_changes = []
|
||||
for idx, (key, value) in enumerate(self.data.items()):
|
||||
if key == 'changes_diagram':
|
||||
pr_body += f"### {PRDescriptionHeader.DIAGRAM_WALKTHROUGH.value}\n\n"
|
||||
pr_body += f"{value}\n\n"
|
||||
continue
|
||||
if key == 'pr_files':
|
||||
value = self.file_label_dict
|
||||
else:
|
||||
@ -597,9 +598,15 @@ class PRDescription:
|
||||
pr_body += f'- `{filename}`: {description}\n'
|
||||
if self.git_provider.is_supported("gfm_markdown"):
|
||||
pr_body += "</details>\n"
|
||||
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
|
||||
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
||||
changes_walkthrough = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH.value}\n{changes_walkthrough}"
|
||||
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types: # 'File Walkthrough' section
|
||||
changes_walkthrough_table, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
||||
if get_settings().pr_description.get('file_table_collapsible_open_by_default', False):
|
||||
initial_status = " open"
|
||||
else:
|
||||
initial_status = ""
|
||||
changes_walkthrough = f"<details{initial_status}> <summary><h3> {PRDescriptionHeader.FILE_WALKTHROUGH.value}</h3></summary>\n\n"
|
||||
changes_walkthrough += f"{changes_walkthrough_table}\n\n"
|
||||
changes_walkthrough += "</details>\n\n"
|
||||
elif key.lower().strip() == 'description':
|
||||
if isinstance(value, list):
|
||||
value = ', '.join(v.rstrip() for v in value)
|
||||
@ -633,14 +640,19 @@ class PRDescription:
|
||||
artifact={"file": file})
|
||||
continue
|
||||
filename = file['filename'].replace("'", "`").replace('"', '`')
|
||||
changes_summary = file.get('changes_summary', "").strip()
|
||||
changes_summary = file.get('changes_summary', "")
|
||||
if not changes_summary:
|
||||
get_logger().warning(f"Empty changes summary in file label dict, skipping file",
|
||||
artifact={"file": file})
|
||||
continue
|
||||
changes_summary = changes_summary.strip()
|
||||
changes_title = file['changes_title'].strip()
|
||||
label = file.get('label').strip().lower()
|
||||
if label not in file_label_dict:
|
||||
file_label_dict[label] = []
|
||||
file_label_dict[label].append((filename, changes_title, changes_summary))
|
||||
except Exception as e:
|
||||
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
|
||||
get_logger().exception(f"Error preparing file label dict {self.pr_id}")
|
||||
pass
|
||||
return file_label_dict
|
||||
|
||||
@ -720,7 +732,7 @@ class PRDescription:
|
||||
pr_body += """</tr></tbody></table>"""
|
||||
|
||||
except Exception as e:
|
||||
get_logger().error(f"Error processing PR files to markdown {self.pr_id}: {str(e)}")
|
||||
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {str(e)}")
|
||||
pass
|
||||
return pr_body, pr_comments
|
||||
|
||||
@ -776,14 +788,21 @@ def insert_br_after_x_chars(text: str, x=70):
|
||||
if count_chars_without_html(text) < x:
|
||||
return text
|
||||
|
||||
is_list = text.lstrip().startswith(("- ", "* "))
|
||||
|
||||
# replace odd instances of ` with <code> and even instances of ` with </code>
|
||||
text = replace_code_tags(text)
|
||||
|
||||
# convert list items to <li>
|
||||
if text.startswith("- ") or text.startswith("* "):
|
||||
text = "<li>" + text[2:]
|
||||
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
||||
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
|
||||
# convert list items to <li> only if the text is identified as a list
|
||||
if is_list:
|
||||
# To handle lists that start with indentation
|
||||
leading_whitespace = text[:len(text) - len(text.lstrip())]
|
||||
body = text.lstrip()
|
||||
body = "<li>" + body[2:]
|
||||
text = leading_whitespace + body
|
||||
|
||||
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
||||
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
|
||||
|
||||
# convert new lines to <br>
|
||||
text = text.replace("\n", '<br>')
|
||||
@ -823,7 +842,13 @@ def insert_br_after_x_chars(text: str, x=70):
|
||||
is_inside_code = True
|
||||
if "</code>" in word:
|
||||
is_inside_code = False
|
||||
return ''.join(new_text).strip()
|
||||
|
||||
processed_text = ''.join(new_text).strip()
|
||||
|
||||
if is_list:
|
||||
processed_text = f"<ul>{processed_text}</ul>"
|
||||
|
||||
return processed_text
|
||||
|
||||
|
||||
def replace_code_tags(text):
|
||||
|
@ -51,7 +51,7 @@ class TestConvertToMarkdown:
|
||||
input_data = {'review': {
|
||||
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
|
||||
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent(f"""\
|
||||
{PRReviewHeader.REGULAR.value} 🔍
|
||||
|
||||
@ -67,12 +67,12 @@ class TestConvertToMarkdown:
|
||||
""")
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
|
||||
def test_simple_dictionary_input_without_gfm_supported(self):
|
||||
input_data = {'review': {
|
||||
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
|
||||
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
@ -89,74 +89,74 @@ class TestConvertToMarkdown:
|
||||
""")
|
||||
|
||||
assert convert_to_markdown_v2(input_data, gfm_supported=False).strip() == expected_output.strip()
|
||||
|
||||
|
||||
def test_key_issues_to_review(self):
|
||||
input_data = {'review': {
|
||||
'key_issues_to_review': [
|
||||
{
|
||||
'relevant_file' : 'src/utils.py',
|
||||
'issue_header' : 'Code Smell',
|
||||
'issue_content' : 'The function is too long and complex.',
|
||||
'relevant_file': 'src/utils.py',
|
||||
'issue_header': 'Code Smell',
|
||||
'issue_content': 'The function is too long and complex.',
|
||||
'start_line': 30,
|
||||
'end_line': 50,
|
||||
}
|
||||
]
|
||||
}}
|
||||
mock_git_provider = Mock()
|
||||
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
|
||||
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
|
||||
mock_git_provider.get_line_link.return_value = reference_link
|
||||
|
||||
expected_output = textwrap.dedent(f"""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>⚡ <strong>Recommended focus areas for review</strong><br><br>
|
||||
|
||||
|
||||
<a href='{reference_link}'><strong>Code Smell</strong></a><br>The function is too long and complex.
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data, git_provider=mock_git_provider).strip() == expected_output.strip()
|
||||
mock_git_provider.get_line_link.assert_called_with('src/utils.py', 30, 50)
|
||||
|
||||
|
||||
def test_ticket_compliance(self):
|
||||
input_data = {'review': {
|
||||
'ticket_compliance_check': [
|
||||
{
|
||||
'ticket_url': 'https://example.com/ticket/123',
|
||||
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'not_compliant_requirements': '',
|
||||
'requires_further_human_verification': '',
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
|
||||
**🎫 Ticket compliance analysis ✅**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**[123](https://example.com/ticket/123) - Fully compliant**
|
||||
|
||||
|
||||
Compliant requirements:
|
||||
|
||||
|
||||
- Requirement 1
|
||||
- Requirement 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
@ -179,43 +179,43 @@ class TestConvertToMarkdown:
|
||||
],
|
||||
'title': 'Bug Fix',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
<table>
|
||||
<tr><td>🔀 <strong>Multiple PR themes</strong><br><br>
|
||||
|
||||
|
||||
<details><summary>
|
||||
Sub-PR theme: <b>Refactoring</b></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
|
||||
Relevant files:
|
||||
|
||||
|
||||
- src/file1.py
|
||||
- src/file2.py
|
||||
___
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary>
|
||||
Sub-PR theme: <b>Bug Fix</b></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
|
||||
Relevant files:
|
||||
|
||||
|
||||
- src/file3.py
|
||||
___
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
@ -228,7 +228,6 @@ class TestConvertToMarkdown:
|
||||
|
||||
expected_output = ''
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
def test_dictionary_with_empty_dictionaries(self):
|
||||
@ -236,16 +235,16 @@ class TestConvertToMarkdown:
|
||||
|
||||
expected_output = ''
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
|
||||
class TestBR:
|
||||
def test_br1(self):
|
||||
file_change_description = '- Imported `FilePatchInfo` and `EDIT_TYPE` from `pr_agent.algo.types` instead of `pr_agent.git_providers.git_provider`.'
|
||||
file_change_description_br = insert_br_after_x_chars(file_change_description)
|
||||
expected_output = ('<li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
|
||||
expected_output = ('<ul><li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
|
||||
'<code>pr_agent.algo.types</code> instead <br>of '
|
||||
'<code>pr_agent.git_providers.git_provider</code>.')
|
||||
'<code>pr_agent.git_providers.git_provider</code>.</ul>')
|
||||
assert file_change_description_br == expected_output
|
||||
# print("-----")
|
||||
# print(file_change_description_br)
|
||||
@ -255,9 +254,9 @@ class TestBR:
|
||||
'- Created a - new -class `ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
|
||||
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection`')
|
||||
file_change_description_br = insert_br_after_x_chars(file_change_description)
|
||||
expected_output = ('<li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
|
||||
expected_output = ('<ul><li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
|
||||
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
|
||||
'</code><br><code>ColorPaletteResourcesCollection</code>')
|
||||
'</code><br><code>ColorPaletteResourcesCollection</code></ul>')
|
||||
assert file_change_description_br == expected_output
|
||||
# print("-----")
|
||||
# print(file_change_description_br)
|
||||
|
Reference in New Issue
Block a user