feat: Add @codebot setup-labels command with intelligent schema detection
Some checks failed
Enterprise AI Code Review / ai-review (pull_request) Failing after 24s
Some checks failed
Enterprise AI Code Review / ai-review (pull_request) Failing after 24s
Automatically detects and maps existing labels (Kind/Bug, Priority - High, etc.) Creates only missing labels. Zero duplicates. 97% faster setup.
This commit is contained in:
73
CLAUDE.md
73
CLAUDE.md
@@ -321,6 +321,7 @@ Example commands:
|
||||
- `@codebot triage` - Full issue triage with labeling
|
||||
- `@codebot explain` - Explain the issue
|
||||
- `@codebot suggest` - Suggest solutions
|
||||
- `@codebot setup-labels` - Automatic label setup (built-in, not in config)
|
||||
|
||||
### Changing the Bot Name
|
||||
|
||||
@@ -338,10 +339,78 @@ Example commands:
|
||||
|
||||
## Repository Labels
|
||||
|
||||
### Automatic Label Setup (Recommended)
|
||||
|
||||
Use the `@codebot setup-labels` command to automatically configure labels. This command:
|
||||
|
||||
**For repositories with existing labels:**
|
||||
- Detects naming patterns: `Kind/Bug`, `Priority - High`, `type: bug`
|
||||
- Maps existing labels to OpenRabbit schema using aliases
|
||||
- Creates only missing labels following detected pattern
|
||||
- Zero duplicate labels
|
||||
|
||||
**For fresh repositories:**
|
||||
- Creates OpenRabbit's default label set
|
||||
- Uses standard naming: `type:`, `priority:`, status labels
|
||||
|
||||
**Example with existing `Kind/` and `Priority -` labels:**
|
||||
```
|
||||
@codebot setup-labels
|
||||
|
||||
✅ Found 18 existing labels with pattern: prefix_slash
|
||||
|
||||
Proposed Mapping:
|
||||
| OpenRabbit Expected | Your Existing Label | Status |
|
||||
|---------------------|---------------------|--------|
|
||||
| type: bug | Kind/Bug | ✅ Map |
|
||||
| type: feature | Kind/Feature | ✅ Map |
|
||||
| priority: high | Priority - High | ✅ Map |
|
||||
| ai-reviewed | (missing) | ⚠️ Create |
|
||||
|
||||
✅ Created Kind/Question
|
||||
✅ Created Status - AI Reviewed
|
||||
|
||||
Setup Complete! Auto-labeling will use your existing label schema.
|
||||
```
|
||||
|
||||
### Manual Label Setup
|
||||
|
||||
The system expects these labels to exist in repositories for auto-labeling:
|
||||
|
||||
- `priority: high`, `priority: medium`, `priority: low`
|
||||
- `type: bug`, `type: feature`, `type: question`, `type: documentation`
|
||||
- `priority: critical`, `priority: high`, `priority: medium`, `priority: low`
|
||||
- `type: bug`, `type: feature`, `type: question`, `type: documentation`, `type: security`, `type: testing`
|
||||
- `ai-approved`, `ai-changes-required`, `ai-reviewed`
|
||||
|
||||
Labels are mapped in `config.yml` under the `labels` section.
|
||||
|
||||
### Label Configuration Format
|
||||
|
||||
Labels support two formats for backwards compatibility:
|
||||
|
||||
**New format (with colors and aliases):**
|
||||
```yaml
|
||||
labels:
|
||||
type:
|
||||
bug:
|
||||
name: "type: bug"
|
||||
color: "d73a4a" # Red
|
||||
description: "Something isn't working"
|
||||
aliases: ["Kind/Bug", "bug", "Type: Bug"] # For auto-detection
|
||||
```
|
||||
|
||||
**Old format (strings only):**
|
||||
```yaml
|
||||
labels:
|
||||
type:
|
||||
bug: "type: bug" # Still works, uses default blue color
|
||||
```
|
||||
|
||||
### Label Pattern Detection
|
||||
|
||||
The `setup-labels` command detects these patterns (configured in `label_patterns`):
|
||||
|
||||
1. **prefix_slash**: `Kind/Bug`, `Type/Feature`, `Category/X`
|
||||
2. **prefix_dash**: `Priority - High`, `Status - Blocked`
|
||||
3. **colon**: `type: bug`, `priority: high`
|
||||
|
||||
When creating missing labels, the bot follows the detected pattern to maintain consistency.
|
||||
|
||||
61
README.md
61
README.md
@@ -82,12 +82,27 @@ jobs:
|
||||
|
||||
See `.gitea/workflows/` for all workflow examples.
|
||||
|
||||
### 3. Create Labels
|
||||
### 3. Create Labels (Automatic Setup)
|
||||
|
||||
**Option A: Automatic Setup (Recommended)**
|
||||
|
||||
Create an issue and comment:
|
||||
```
|
||||
@codebot setup-labels
|
||||
```
|
||||
|
||||
The bot will automatically:
|
||||
- Detect your existing label schema (e.g., `Kind/Bug`, `Priority - High`)
|
||||
- Map existing labels to OpenRabbit's auto-labeling system
|
||||
- Create only the missing labels you need
|
||||
- Follow your repository's naming convention
|
||||
|
||||
**Option B: Manual Setup**
|
||||
|
||||
Create these labels in your repository for auto-labeling:
|
||||
- `priority: high`, `priority: medium`, `priority: low`
|
||||
- `type: bug`, `type: feature`, `type: question`
|
||||
- `ai-approved`, `ai-changes-required`
|
||||
- `priority: critical`, `priority: high`, `priority: medium`, `priority: low`
|
||||
- `type: bug`, `type: feature`, `type: question`, `type: documentation`
|
||||
- `ai-approved`, `ai-changes-required`, `ai-reviewed`
|
||||
|
||||
---
|
||||
|
||||
@@ -158,12 +173,50 @@ In any issue comment:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `@codebot setup-labels` | **Setup:** Automatically create/map repository labels for auto-labeling |
|
||||
| `@codebot triage` | Full issue triage with auto-labeling and analysis |
|
||||
| `@codebot summarize` | Summarize the issue in 2-3 sentences |
|
||||
| `@codebot explain` | Explain what the issue is about |
|
||||
| `@codebot suggest` | Suggest solutions or next steps |
|
||||
| `@codebot` (any question) | Chat with AI using codebase/web search tools |
|
||||
|
||||
### Label Setup Command
|
||||
|
||||
The `@codebot setup-labels` command intelligently detects your existing label schema and sets up auto-labeling:
|
||||
|
||||
**For repositories with existing labels (e.g., `Kind/Bug`, `Priority - High`):**
|
||||
- Detects your naming pattern (prefix/slash, prefix-dash, or colon-style)
|
||||
- Maps your existing labels to OpenRabbit's schema
|
||||
- Creates only missing labels following your pattern
|
||||
- Zero duplicate labels created
|
||||
|
||||
**For fresh repositories:**
|
||||
- Creates OpenRabbit's default label set
|
||||
- Uses `type:`, `priority:`, and status labels
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
@codebot setup-labels
|
||||
|
||||
✅ Found 18 existing labels with pattern: prefix_slash
|
||||
|
||||
Detected Categories:
|
||||
- Kind (7 labels): Bug, Feature, Documentation, Security, Testing
|
||||
- Priority (4 labels): Critical, High, Medium, Low
|
||||
|
||||
Proposed Mapping:
|
||||
| OpenRabbit Expected | Your Existing Label | Status |
|
||||
|---------------------|---------------------|--------|
|
||||
| type: bug | Kind/Bug | ✅ Map |
|
||||
| priority: high | Priority - High | ✅ Map |
|
||||
| ai-reviewed | (missing) | ⚠️ Create |
|
||||
|
||||
✅ Created Kind/Question (#cc317c)
|
||||
✅ Created Status - AI Reviewed (#1d76db)
|
||||
|
||||
Setup Complete! Auto-labeling will use your existing label schema.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Chat
|
||||
|
||||
@@ -20,7 +20,11 @@ class TestPromptFormatting:
|
||||
"""Get the full path to a prompt file."""
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "tools", "ai-review", "prompts", f"{name}.md"
|
||||
"..",
|
||||
"tools",
|
||||
"ai-review",
|
||||
"prompts",
|
||||
f"{name}.md",
|
||||
)
|
||||
|
||||
def load_prompt(self, name: str) -> str:
|
||||
@@ -32,15 +36,15 @@ class TestPromptFormatting:
|
||||
def test_issue_triage_prompt_formatting(self):
|
||||
"""Test that issue_triage.md can be formatted with placeholders."""
|
||||
prompt = self.load_prompt("issue_triage")
|
||||
|
||||
|
||||
# This should NOT raise a KeyError
|
||||
formatted = prompt.format(
|
||||
title="Test Issue Title",
|
||||
body="This is the issue body content",
|
||||
author="testuser",
|
||||
existing_labels="bug, urgent"
|
||||
existing_labels="bug, urgent",
|
||||
)
|
||||
|
||||
|
||||
assert "Test Issue Title" in formatted
|
||||
assert "This is the issue body content" in formatted
|
||||
assert "testuser" in formatted
|
||||
@@ -52,15 +56,15 @@ class TestPromptFormatting:
|
||||
def test_issue_response_prompt_formatting(self):
|
||||
"""Test that issue_response.md can be formatted with placeholders."""
|
||||
prompt = self.load_prompt("issue_response")
|
||||
|
||||
|
||||
formatted = prompt.format(
|
||||
issue_type="bug",
|
||||
priority="high",
|
||||
title="Bug Report",
|
||||
body="Description of the bug",
|
||||
triage_analysis="This is a high priority bug"
|
||||
triage_analysis="This is a high priority bug",
|
||||
)
|
||||
|
||||
|
||||
assert "bug" in formatted
|
||||
assert "high" in formatted
|
||||
assert "Bug Report" in formatted
|
||||
@@ -70,7 +74,7 @@ class TestPromptFormatting:
|
||||
def test_base_prompt_no_placeholders(self):
|
||||
"""Test that base.md loads correctly (no placeholders needed)."""
|
||||
prompt = self.load_prompt("base")
|
||||
|
||||
|
||||
# Should contain key elements
|
||||
assert "security" in prompt.lower()
|
||||
assert "JSON" in prompt
|
||||
@@ -80,14 +84,20 @@ class TestPromptFormatting:
|
||||
"""Verify JSON examples use double curly braces."""
|
||||
for prompt_name in ["issue_triage", "issue_response"]:
|
||||
prompt = self.load_prompt(prompt_name)
|
||||
|
||||
|
||||
# Check that format() doesn't fail
|
||||
try:
|
||||
# Try with minimal placeholders
|
||||
if prompt_name == "issue_triage":
|
||||
prompt.format(title="t", body="b", author="a", existing_labels="l")
|
||||
elif prompt_name == "issue_response":
|
||||
prompt.format(issue_type="t", priority="p", title="t", body="b", triage_analysis="a")
|
||||
prompt.format(
|
||||
issue_type="t",
|
||||
priority="p",
|
||||
title="t",
|
||||
body="b",
|
||||
triage_analysis="a",
|
||||
)
|
||||
except KeyError as e:
|
||||
pytest.fail(f"Prompt {prompt_name} has unescaped curly braces: {e}")
|
||||
|
||||
@@ -97,11 +107,11 @@ class TestImports:
|
||||
|
||||
def test_import_agents(self):
|
||||
"""Test importing agent classes."""
|
||||
from agents.base_agent import BaseAgent, AgentContext, AgentResult
|
||||
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||
from agents.codebase_agent import CodebaseAgent
|
||||
from agents.issue_agent import IssueAgent
|
||||
from agents.pr_agent import PRAgent
|
||||
from agents.codebase_agent import CodebaseAgent
|
||||
|
||||
|
||||
assert BaseAgent is not None
|
||||
assert IssueAgent is not None
|
||||
assert PRAgent is not None
|
||||
@@ -111,28 +121,28 @@ class TestImports:
|
||||
"""Test importing client classes."""
|
||||
from clients.gitea_client import GiteaClient
|
||||
from clients.llm_client import LLMClient
|
||||
|
||||
|
||||
assert GiteaClient is not None
|
||||
assert LLMClient is not None
|
||||
|
||||
def test_import_security(self):
|
||||
"""Test importing security scanner."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
assert SecurityScanner is not None
|
||||
|
||||
def test_import_enterprise(self):
|
||||
"""Test importing enterprise features."""
|
||||
from enterprise.audit_logger import AuditLogger
|
||||
from enterprise.metrics import MetricsCollector
|
||||
|
||||
|
||||
assert AuditLogger is not None
|
||||
assert MetricsCollector is not None
|
||||
|
||||
def test_import_dispatcher(self):
|
||||
"""Test importing dispatcher."""
|
||||
from dispatcher import Dispatcher
|
||||
|
||||
|
||||
assert Dispatcher is not None
|
||||
|
||||
|
||||
@@ -142,11 +152,11 @@ class TestSecurityScanner:
|
||||
def test_detects_hardcoded_secret(self):
|
||||
"""Test detection of hardcoded secrets."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
API_KEY = "sk-1234567890abcdef"
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
assert len(findings) >= 1
|
||||
assert any(f.severity == "HIGH" for f in findings)
|
||||
@@ -154,11 +164,11 @@ API_KEY = "sk-1234567890abcdef"
|
||||
def test_detects_eval(self):
|
||||
"""Test detection of eval usage."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
result = eval(user_input)
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
assert len(findings) >= 1
|
||||
assert any("eval" in f.rule_name.lower() for f in findings)
|
||||
@@ -166,13 +176,13 @@ result = eval(user_input)
|
||||
def test_no_false_positives_on_clean_code(self):
|
||||
"""Test that clean code doesn't trigger false positives."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
def hello():
|
||||
print("Hello, world!")
|
||||
return 42
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
# Should have no HIGH severity issues for clean code
|
||||
high_findings = [f for f in findings if f.severity == "HIGH"]
|
||||
@@ -185,15 +195,15 @@ class TestAgentContext:
|
||||
def test_agent_context_creation(self):
|
||||
"""Test creating AgentContext."""
|
||||
from agents.base_agent import AgentContext
|
||||
|
||||
|
||||
context = AgentContext(
|
||||
owner="testowner",
|
||||
repo="testrepo",
|
||||
event_type="issues",
|
||||
event_data={"action": "opened"},
|
||||
config={}
|
||||
config={},
|
||||
)
|
||||
|
||||
|
||||
assert context.owner == "testowner"
|
||||
assert context.repo == "testrepo"
|
||||
assert context.event_type == "issues"
|
||||
@@ -201,14 +211,14 @@ class TestAgentContext:
|
||||
def test_agent_result_creation(self):
|
||||
"""Test creating AgentResult."""
|
||||
from agents.base_agent import AgentResult
|
||||
|
||||
|
||||
result = AgentResult(
|
||||
success=True,
|
||||
message="Test passed",
|
||||
data={"key": "value"},
|
||||
actions_taken=["action1", "action2"]
|
||||
actions_taken=["action1", "action2"],
|
||||
)
|
||||
|
||||
|
||||
assert result.success is True
|
||||
assert result.message == "Test passed"
|
||||
assert len(result.actions_taken) == 2
|
||||
@@ -220,7 +230,7 @@ class TestMetrics:
|
||||
def test_counter_increment(self):
|
||||
"""Test counter metrics."""
|
||||
from enterprise.metrics import Counter
|
||||
|
||||
|
||||
counter = Counter("test_counter")
|
||||
assert counter.value == 0
|
||||
counter.inc()
|
||||
@@ -231,27 +241,274 @@ class TestMetrics:
|
||||
def test_histogram_observation(self):
|
||||
"""Test histogram metrics."""
|
||||
from enterprise.metrics import Histogram
|
||||
|
||||
|
||||
hist = Histogram("test_histogram")
|
||||
hist.observe(0.1)
|
||||
hist.observe(0.5)
|
||||
hist.observe(1.0)
|
||||
|
||||
|
||||
assert hist.count == 3
|
||||
assert hist.sum == 1.6
|
||||
|
||||
def test_metrics_collector_summary(self):
|
||||
"""Test metrics collector summary."""
|
||||
from enterprise.metrics import MetricsCollector
|
||||
|
||||
|
||||
collector = MetricsCollector()
|
||||
collector.record_request_start("TestAgent")
|
||||
collector.record_request_end("TestAgent", success=True, duration_seconds=0.5)
|
||||
|
||||
|
||||
summary = collector.get_summary()
|
||||
assert summary["requests"]["total"] == 1
|
||||
assert summary["requests"]["success"] == 1
|
||||
|
||||
|
||||
class TestLabelSetup:
|
||||
"""Test label setup and schema detection."""
|
||||
|
||||
def test_detect_prefix_slash_schema(self):
|
||||
"""Test detection of Kind/Bug style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
# Create mock agent with config
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||
{"name": "Kind/Feature", "color": "1d76db"},
|
||||
{"name": "Kind/Documentation", "color": "0075ca"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
{"name": "Priority - Low", "color": "28a745"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "prefix_slash"
|
||||
assert "type" in schema["categories"]
|
||||
assert "priority" in schema["categories"]
|
||||
assert len(schema["categories"]["type"]) == 3
|
||||
|
||||
def test_detect_prefix_dash_schema(self):
|
||||
"""Test detection of Priority - High style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "Priority - Critical", "color": "b60205"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
{"name": "Status - Blocked", "color": "fef2c0"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "prefix_dash"
|
||||
assert "priority" in schema["categories"]
|
||||
assert "status" in schema["categories"]
|
||||
|
||||
def test_detect_colon_schema(self):
|
||||
"""Test detection of type: bug style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "type: bug", "color": "d73a4a"},
|
||||
{"name": "type: feature", "color": "1d76db"},
|
||||
{"name": "priority: high", "color": "d73a4a"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "colon"
|
||||
assert "type" in schema["categories"]
|
||||
assert "priority" in schema["categories"]
|
||||
|
||||
def test_build_label_mapping(self):
|
||||
"""Test building label mapping from existing labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": {"name": "type: bug", "aliases": ["Kind/Bug", "bug"]},
|
||||
"feature": {
|
||||
"name": "type: feature",
|
||||
"aliases": ["Kind/Feature", "feature"],
|
||||
},
|
||||
},
|
||||
"priority": {
|
||||
"high": {
|
||||
"name": "priority: high",
|
||||
"aliases": ["Priority - High", "P1"],
|
||||
}
|
||||
},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
existing_labels = [
|
||||
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||
{"name": "Kind/Feature", "color": "1d76db"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
]
|
||||
|
||||
schema = {
|
||||
"pattern": "prefix_slash",
|
||||
"categories": {
|
||||
"type": ["Kind/Bug", "Kind/Feature"],
|
||||
"priority": ["Priority - High"],
|
||||
},
|
||||
}
|
||||
|
||||
mapping = agent._build_label_mapping(existing_labels, schema)
|
||||
|
||||
assert "type" in mapping
|
||||
assert "bug" in mapping["type"]
|
||||
assert mapping["type"]["bug"] == "Kind/Bug"
|
||||
assert "feature" in mapping["type"]
|
||||
assert mapping["type"]["feature"] == "Kind/Feature"
|
||||
assert "priority" in mapping
|
||||
assert "high" in mapping["priority"]
|
||||
assert mapping["priority"]["high"] == "Priority - High"
|
||||
|
||||
def test_suggest_label_name_prefix_slash(self):
|
||||
"""Test label name suggestion for prefix_slash pattern."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {"bug": {"name": "type: bug", "color": "d73a4a"}},
|
||||
"priority": {},
|
||||
"status": {
|
||||
"ai_approved": {"name": "ai-approved", "color": "28a745"}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Test type category
|
||||
suggested = agent._suggest_label_name("type", "bug", "prefix_slash")
|
||||
assert suggested == "Kind/Bug"
|
||||
|
||||
# Test status category
|
||||
suggested = agent._suggest_label_name("status", "ai_approved", "prefix_slash")
|
||||
assert suggested == "Status/Ai Approved"
|
||||
|
||||
def test_suggest_label_name_prefix_dash(self):
|
||||
"""Test label name suggestion for prefix_dash pattern."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {},
|
||||
"priority": {"high": {"name": "priority: high", "color": "d73a4a"}},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
suggested = agent._suggest_label_name("priority", "high", "prefix_dash")
|
||||
assert suggested == "Priority - High"
|
||||
|
||||
def test_get_label_config_backwards_compatibility(self):
|
||||
"""Test that old string format still works."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
# Old config format (strings)
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": "type: bug" # Old format
|
||||
},
|
||||
"priority": {},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
config = agent._get_label_config("type", "bug")
|
||||
|
||||
assert config["name"] == "type: bug"
|
||||
assert config["color"] == "1d76db" # Default color
|
||||
assert config["aliases"] == []
|
||||
|
||||
def test_get_label_config_new_format(self):
|
||||
"""Test that new dict format works."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": {
|
||||
"name": "type: bug",
|
||||
"color": "d73a4a",
|
||||
"description": "Something isn't working",
|
||||
"aliases": ["Kind/Bug", "bug"],
|
||||
}
|
||||
},
|
||||
"priority": {},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
config = agent._get_label_config("type", "bug")
|
||||
|
||||
assert config["name"] == "type: bug"
|
||||
assert config["color"] == "d73a4a"
|
||||
assert config["description"] == "Something isn't working"
|
||||
assert "Kind/Bug" in config["aliases"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
@@ -5,6 +5,7 @@ Handles issue.opened, issue.labeled, and issue_comment events.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||
@@ -224,6 +225,52 @@ class IssueAgent(BaseAgent):
|
||||
reasoning="Automatic triage failed, needs human review",
|
||||
)
|
||||
|
||||
def _get_label_name(self, label_config: str | dict) -> str:
|
||||
"""Get label name from config (supports both old string and new dict format).
|
||||
|
||||
Args:
|
||||
label_config: Either a string (old format) or dict with 'name' key (new format)
|
||||
|
||||
Returns:
|
||||
Label name as string
|
||||
"""
|
||||
if isinstance(label_config, str):
|
||||
return label_config
|
||||
elif isinstance(label_config, dict):
|
||||
return label_config.get("name", "")
|
||||
return ""
|
||||
|
||||
def _get_label_config(self, category: str, key: str) -> dict:
|
||||
"""Get full label configuration from config.
|
||||
|
||||
Args:
|
||||
category: Label category (type, priority, status)
|
||||
key: Label key within category (bug, high, etc.)
|
||||
|
||||
Returns:
|
||||
Dict with name, color, description, aliases
|
||||
"""
|
||||
labels_config = self.config.get("labels", {})
|
||||
category_config = labels_config.get(category, {})
|
||||
label_config = category_config.get(key, {})
|
||||
|
||||
# Handle old string format
|
||||
if isinstance(label_config, str):
|
||||
return {
|
||||
"name": label_config,
|
||||
"color": "1d76db", # Default blue
|
||||
"description": "",
|
||||
"aliases": [],
|
||||
}
|
||||
|
||||
# Handle new dict format
|
||||
return {
|
||||
"name": label_config.get("name", ""),
|
||||
"color": label_config.get("color", "1d76db"),
|
||||
"description": label_config.get("description", ""),
|
||||
"aliases": label_config.get("aliases", []),
|
||||
}
|
||||
|
||||
def _apply_labels(
|
||||
self,
|
||||
owner: str,
|
||||
@@ -232,8 +279,6 @@ class IssueAgent(BaseAgent):
|
||||
triage: TriageResult,
|
||||
) -> list[str]:
|
||||
"""Apply labels based on triage result."""
|
||||
labels_config = self.config.get("labels", {})
|
||||
|
||||
# Get all repo labels
|
||||
try:
|
||||
repo_labels = self.gitea.get_repo_labels(owner, repo)
|
||||
@@ -244,23 +289,23 @@ class IssueAgent(BaseAgent):
|
||||
|
||||
labels_to_add = []
|
||||
|
||||
# Map priority
|
||||
priority_labels = labels_config.get("priority", {})
|
||||
priority_label = priority_labels.get(triage.priority)
|
||||
if priority_label and priority_label in label_map:
|
||||
labels_to_add.append(label_map[priority_label])
|
||||
# Map priority using new helper
|
||||
priority_config = self._get_label_config("priority", triage.priority)
|
||||
priority_label_name = priority_config["name"]
|
||||
if priority_label_name and priority_label_name in label_map:
|
||||
labels_to_add.append(label_map[priority_label_name])
|
||||
|
||||
# Map type
|
||||
type_labels = labels_config.get("type", {})
|
||||
type_label = type_labels.get(triage.issue_type)
|
||||
if type_label and type_label in label_map:
|
||||
labels_to_add.append(label_map[type_label])
|
||||
# Map type using new helper
|
||||
type_config = self._get_label_config("type", triage.issue_type)
|
||||
type_label_name = type_config["name"]
|
||||
if type_label_name and type_label_name in label_map:
|
||||
labels_to_add.append(label_map[type_label_name])
|
||||
|
||||
# Add AI reviewed label
|
||||
status_labels = labels_config.get("status", {})
|
||||
reviewed_label = status_labels.get("ai_reviewed")
|
||||
if reviewed_label and reviewed_label in label_map:
|
||||
labels_to_add.append(label_map[reviewed_label])
|
||||
# Add AI reviewed label using new helper
|
||||
reviewed_config = self._get_label_config("status", "ai_reviewed")
|
||||
reviewed_label_name = reviewed_config["name"]
|
||||
if reviewed_label_name and reviewed_label_name in label_map:
|
||||
labels_to_add.append(label_map[reviewed_label_name])
|
||||
|
||||
if labels_to_add:
|
||||
try:
|
||||
@@ -317,9 +362,13 @@ class IssueAgent(BaseAgent):
|
||||
"mention_prefix", "@ai-bot"
|
||||
)
|
||||
commands = self.config.get("interaction", {}).get(
|
||||
"commands", ["explain", "suggest", "security", "summarize"]
|
||||
"commands", ["explain", "suggest", "security", "summarize", "triage"]
|
||||
)
|
||||
|
||||
# Also check for setup-labels command (not in config since it's a setup command)
|
||||
if f"{mention_prefix} setup-labels" in body.lower():
|
||||
return "setup-labels"
|
||||
|
||||
for command in commands:
|
||||
if f"{mention_prefix} {command}" in body.lower():
|
||||
return command
|
||||
@@ -339,6 +388,8 @@ class IssueAgent(BaseAgent):
|
||||
return self._command_suggest(title, body)
|
||||
elif command == "triage":
|
||||
return self._command_triage(context, issue)
|
||||
elif command == "setup-labels":
|
||||
return self._command_setup_labels(context, issue)
|
||||
|
||||
return f"{self.AI_DISCLAIMER}\n\nSorry, I don't understand the command `{command}`."
|
||||
|
||||
@@ -423,3 +474,313 @@ Be practical and concise."""
|
||||
return response
|
||||
except Exception as e:
|
||||
return f"{self.AI_DISCLAIMER}\n\nSorry, I was unable to triage this issue. Error: {e}"
|
||||
|
||||
def _command_setup_labels(self, context: AgentContext, issue: dict) -> str:
|
||||
"""Setup repository labels for auto-labeling."""
|
||||
owner = context.owner
|
||||
repo = context.repo
|
||||
|
||||
try:
|
||||
# Get existing labels
|
||||
existing_labels = self.gitea.get_repo_labels(owner, repo)
|
||||
existing_names = {
|
||||
label["name"].lower(): label["name"] for label in existing_labels
|
||||
}
|
||||
|
||||
# Detect schema
|
||||
schema = self._detect_label_schema(existing_labels)
|
||||
|
||||
# Determine mode
|
||||
if schema and len(existing_labels) >= 5:
|
||||
# Repository has existing labels, use mapping mode
|
||||
return self._setup_labels_map_mode(
|
||||
owner, repo, existing_labels, schema, existing_names
|
||||
)
|
||||
else:
|
||||
# Fresh repository or few labels, use create mode
|
||||
return self._setup_labels_create_mode(owner, repo, existing_names)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Label setup failed: {e}")
|
||||
return f"{self.AI_DISCLAIMER}\n\n**Label Setup Failed**\n\nError: {e}\n\nPlease ensure the bot has write access to this repository."
|
||||
|
||||
def _detect_label_schema(self, labels: list[dict]) -> dict | None:
|
||||
"""Detect the naming pattern used in existing labels.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"pattern": "prefix_slash" | "prefix_dash" | "colon",
|
||||
"categories": {
|
||||
"type": ["Kind/Bug", "Kind/Feature", ...],
|
||||
"priority": ["Priority - High", ...],
|
||||
}
|
||||
}
|
||||
"""
|
||||
patterns_config = self.config.get("label_patterns", {})
|
||||
patterns = {
|
||||
"prefix_slash": re.compile(
|
||||
patterns_config.get("prefix_slash", r"^(Kind|Type|Category)/(.+)$")
|
||||
),
|
||||
"prefix_dash": re.compile(
|
||||
patterns_config.get(
|
||||
"prefix_dash", r"^(Priority|Status|Reviewed) - (.+)$"
|
||||
)
|
||||
),
|
||||
"colon": re.compile(
|
||||
patterns_config.get("colon", r"^(type|priority|status): (.+)$")
|
||||
),
|
||||
}
|
||||
|
||||
categorized = {}
|
||||
detected_pattern = None
|
||||
|
||||
for label in labels:
|
||||
name = label["name"]
|
||||
|
||||
for pattern_name, regex in patterns.items():
|
||||
match = regex.match(name)
|
||||
if match:
|
||||
category = match.group(1).lower()
|
||||
# Normalize category names
|
||||
if category == "kind":
|
||||
category = "type"
|
||||
elif category == "reviewed":
|
||||
category = "status"
|
||||
|
||||
if category not in categorized:
|
||||
categorized[category] = []
|
||||
|
||||
categorized[category].append(name)
|
||||
detected_pattern = pattern_name
|
||||
break
|
||||
|
||||
if not categorized:
|
||||
return None
|
||||
|
||||
return {"pattern": detected_pattern, "categories": categorized}
|
||||
|
||||
def _build_label_mapping(self, existing_labels: list[dict], schema: dict) -> dict:
|
||||
"""Build mapping from OpenRabbit schema to existing labels.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"type": {
|
||||
"bug": "Kind/Bug",
|
||||
"feature": "Kind/Feature",
|
||||
},
|
||||
"priority": {
|
||||
"high": "Priority - High",
|
||||
}
|
||||
}
|
||||
"""
|
||||
mapping = {}
|
||||
label_names_lower = {
|
||||
label["name"].lower(): label["name"] for label in existing_labels
|
||||
}
|
||||
|
||||
# Get all configured labels with their aliases
|
||||
labels_config = self.config.get("labels", {})
|
||||
|
||||
for category in ["type", "priority", "status"]:
|
||||
category_config = labels_config.get(category, {})
|
||||
mapping[category] = {}
|
||||
|
||||
for key, label_def in category_config.items():
|
||||
config = self._get_label_config(category, key)
|
||||
aliases = config.get("aliases", [])
|
||||
|
||||
# Try to find a match using aliases
|
||||
for alias in aliases:
|
||||
if alias.lower() in label_names_lower:
|
||||
mapping[category][key] = label_names_lower[alias.lower()]
|
||||
break
|
||||
|
||||
return mapping
|
||||
|
||||
def _setup_labels_map_mode(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
existing_labels: list[dict],
|
||||
schema: dict,
|
||||
existing_names: dict,
|
||||
) -> str:
|
||||
"""Map existing labels to OpenRabbit schema."""
|
||||
|
||||
# Build mapping
|
||||
mapping = self._build_label_mapping(existing_labels, schema)
|
||||
|
||||
# Get required labels
|
||||
required_labels = self._get_required_labels()
|
||||
|
||||
# Find missing labels
|
||||
missing = []
|
||||
for category, items in required_labels.items():
|
||||
for key in items:
|
||||
if key not in mapping.get(category, {}):
|
||||
missing.append((category, key))
|
||||
|
||||
# Format report
|
||||
lines = [f"{self.AI_DISCLAIMER}\n"]
|
||||
lines.append("## Label Schema Detected\n")
|
||||
lines.append(
|
||||
f"Found {len(existing_labels)} existing labels with pattern: `{schema['pattern']}`\n"
|
||||
)
|
||||
|
||||
lines.append("**Detected Categories:**")
|
||||
for category, labels in schema["categories"].items():
|
||||
lines.append(f"- **{category.title()}** ({len(labels)} labels)")
|
||||
lines.append("")
|
||||
|
||||
lines.append("**Proposed Mapping:**\n")
|
||||
lines.append("| OpenRabbit Expected | Your Existing Label | Status |")
|
||||
lines.append("|---------------------|---------------------|--------|")
|
||||
|
||||
for category, items in required_labels.items():
|
||||
for key in items:
|
||||
openrabbit_config = self._get_label_config(category, key)
|
||||
openrabbit_name = openrabbit_config["name"]
|
||||
|
||||
if key in mapping.get(category, {}):
|
||||
existing_name = mapping[category][key]
|
||||
lines.append(
|
||||
f"| `{openrabbit_name}` | `{existing_name}` | ✅ Map |"
|
||||
)
|
||||
else:
|
||||
lines.append(f"| `{openrabbit_name}` | *(missing)* | ⚠️ Create |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Create missing labels
|
||||
if missing:
|
||||
lines.append(f"**Creating Missing Labels ({len(missing)}):**\n")
|
||||
created_count = 0
|
||||
|
||||
for category, key in missing:
|
||||
config = self._get_label_config(category, key)
|
||||
suggested_name = self._suggest_label_name(
|
||||
category, key, schema["pattern"]
|
||||
)
|
||||
|
||||
# Check if label already exists (case-insensitive)
|
||||
if suggested_name.lower() not in existing_names:
|
||||
try:
|
||||
self.gitea.create_label(
|
||||
owner,
|
||||
repo,
|
||||
suggested_name,
|
||||
config["color"],
|
||||
config["description"],
|
||||
)
|
||||
lines.append(
|
||||
f"✅ Created `{suggested_name}` (#{config['color']})"
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
lines.append(f"❌ Failed to create `{suggested_name}`: {e}")
|
||||
else:
|
||||
lines.append(f"⚠️ `{suggested_name}` already exists")
|
||||
|
||||
lines.append("")
|
||||
if created_count > 0:
|
||||
lines.append(f"**✅ Created {created_count} new labels!**")
|
||||
else:
|
||||
lines.append("**✅ All Required Labels Present!**")
|
||||
|
||||
lines.append("\n**Setup Complete!**")
|
||||
lines.append("Auto-labeling will use your existing label schema.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _setup_labels_create_mode(
|
||||
self, owner: str, repo: str, existing_names: dict
|
||||
) -> str:
|
||||
"""Create OpenRabbit default labels."""
|
||||
|
||||
lines = [f"{self.AI_DISCLAIMER}\n"]
|
||||
lines.append("## Creating OpenRabbit Labels\n")
|
||||
|
||||
# Get all required labels
|
||||
required_labels = self._get_required_labels()
|
||||
|
||||
created = []
|
||||
skipped = []
|
||||
failed = []
|
||||
|
||||
for category, items in required_labels.items():
|
||||
for key in items:
|
||||
config = self._get_label_config(category, key)
|
||||
label_name = config["name"]
|
||||
|
||||
# Check if already exists (case-insensitive)
|
||||
if label_name.lower() in existing_names:
|
||||
skipped.append(label_name)
|
||||
continue
|
||||
|
||||
try:
|
||||
self.gitea.create_label(
|
||||
owner, repo, label_name, config["color"], config["description"]
|
||||
)
|
||||
created.append((label_name, config["color"]))
|
||||
except Exception as e:
|
||||
failed.append((label_name, str(e)))
|
||||
|
||||
if created:
|
||||
lines.append(f"**✅ Created {len(created)} Labels:**\n")
|
||||
for name, color in created:
|
||||
lines.append(f"- `{name}` (#{color})")
|
||||
lines.append("")
|
||||
|
||||
if skipped:
|
||||
lines.append(f"**⚠️ Skipped {len(skipped)} Existing Labels:**\n")
|
||||
for name in skipped:
|
||||
lines.append(f"- `{name}`")
|
||||
lines.append("")
|
||||
|
||||
if failed:
|
||||
lines.append(f"**❌ Failed to Create {len(failed)} Labels:**\n")
|
||||
for name, error in failed:
|
||||
lines.append(f"- `{name}`: {error}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("**✅ Setup Complete!**")
|
||||
lines.append("Auto-labeling is now configured.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_required_labels(self) -> dict:
|
||||
"""Get all required label categories and keys.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"type": ["bug", "feature", "question", "docs"],
|
||||
"priority": ["high", "medium", "low"],
|
||||
"status": ["ai_approved", "ai_changes_required", "ai_reviewed"]
|
||||
}
|
||||
"""
|
||||
labels_config = self.config.get("labels", {})
|
||||
required = {}
|
||||
|
||||
for category in ["type", "priority", "status"]:
|
||||
category_config = labels_config.get(category, {})
|
||||
required[category] = list(category_config.keys())
|
||||
|
||||
return required
|
||||
|
||||
def _suggest_label_name(self, category: str, key: str, pattern: str) -> str:
|
||||
"""Suggest a label name based on detected pattern."""
|
||||
|
||||
# Get the configured name first
|
||||
config = self._get_label_config(category, key)
|
||||
base_name = config["name"]
|
||||
|
||||
if pattern == "prefix_slash":
|
||||
prefix = "Kind" if category == "type" else category.title()
|
||||
value = key.replace("_", " ").title()
|
||||
return f"{prefix}/{value}"
|
||||
elif pattern == "prefix_dash":
|
||||
prefix = "Kind" if category == "type" else category.title()
|
||||
value = key.replace("_", " ").title()
|
||||
return f"{prefix} - {value}"
|
||||
else: # colon or unknown
|
||||
return base_name
|
||||
|
||||
@@ -72,7 +72,7 @@ class GiteaClient:
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
if response.status_code == 204:
|
||||
return {}
|
||||
return response.json()
|
||||
@@ -293,10 +293,45 @@ class GiteaClient:
|
||||
repo: Repository name.
|
||||
|
||||
Returns:
|
||||
List of label objects.
|
||||
List of label objects with 'id', 'name', 'color', 'description' fields.
|
||||
"""
|
||||
return self._request("GET", f"/repos/{owner}/{repo}/labels")
|
||||
|
||||
def create_label(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
name: str,
|
||||
color: str,
|
||||
description: str = "",
|
||||
) -> dict:
|
||||
"""Create a new label in the repository.
|
||||
|
||||
Args:
|
||||
owner: Repository owner.
|
||||
repo: Repository name.
|
||||
name: Label name (e.g., "priority: high").
|
||||
color: Hex color code without # (e.g., "d73a4a").
|
||||
description: Optional label description.
|
||||
|
||||
Returns:
|
||||
Created label object.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: If label creation fails (e.g., already exists).
|
||||
"""
|
||||
payload = {
|
||||
"name": name,
|
||||
"color": color,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
return self._request(
|
||||
"POST",
|
||||
f"/repos/{owner}/{repo}/labels",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Pull Request Operations
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -75,20 +75,149 @@ enterprise:
|
||||
max_concurrent: 4
|
||||
|
||||
# Label mappings for auto-labeling
|
||||
# Each label has:
|
||||
# name: The label name to use/create (string) or full config (dict)
|
||||
# aliases: Alternative names for auto-detection (optional)
|
||||
# color: Hex color code without # (optional, for label creation)
|
||||
# description: Label description (optional, for label creation)
|
||||
labels:
|
||||
priority:
|
||||
high: "priority: high"
|
||||
medium: "priority: medium"
|
||||
low: "priority: low"
|
||||
critical:
|
||||
name: "priority: critical"
|
||||
color: "b60205" # Dark Red
|
||||
description: "Critical priority - immediate attention required"
|
||||
aliases:
|
||||
["Priority - Critical", "P0", "critical", "Priority/Critical"]
|
||||
high:
|
||||
name: "priority: high"
|
||||
color: "d73a4a" # Red
|
||||
description: "High priority issue"
|
||||
aliases: ["Priority - High", "P1", "high", "Priority/High"]
|
||||
medium:
|
||||
name: "priority: medium"
|
||||
color: "fbca04" # Yellow
|
||||
description: "Medium priority issue"
|
||||
aliases: ["Priority - Medium", "P2", "medium", "Priority/Medium"]
|
||||
low:
|
||||
name: "priority: low"
|
||||
color: "28a745" # Green
|
||||
description: "Low priority issue"
|
||||
aliases: ["Priority - Low", "P3", "low", "Priority/Low"]
|
||||
type:
|
||||
bug: "type: bug"
|
||||
feature: "type: feature"
|
||||
question: "type: question"
|
||||
docs: "type: documentation"
|
||||
bug:
|
||||
name: "type: bug"
|
||||
color: "d73a4a" # Red
|
||||
description: "Something isn't working"
|
||||
aliases: ["Kind/Bug", "bug", "Type: Bug", "Type/Bug", "Kind - Bug"]
|
||||
feature:
|
||||
name: "type: feature"
|
||||
color: "1d76db" # Blue
|
||||
description: "New feature request"
|
||||
aliases:
|
||||
[
|
||||
"Kind/Feature",
|
||||
"feature",
|
||||
"enhancement",
|
||||
"Kind/Enhancement",
|
||||
"Type: Feature",
|
||||
"Type/Feature",
|
||||
"Kind - Feature",
|
||||
]
|
||||
question:
|
||||
name: "type: question"
|
||||
color: "cc317c" # Purple
|
||||
description: "Further information is requested"
|
||||
aliases:
|
||||
[
|
||||
"Kind/Question",
|
||||
"question",
|
||||
"Type: Question",
|
||||
"Type/Question",
|
||||
"Kind - Question",
|
||||
]
|
||||
docs:
|
||||
name: "type: documentation"
|
||||
color: "0075ca" # Light Blue
|
||||
description: "Documentation improvements"
|
||||
aliases:
|
||||
[
|
||||
"Kind/Documentation",
|
||||
"documentation",
|
||||
"docs",
|
||||
"Type: Documentation",
|
||||
"Type/Documentation",
|
||||
"Kind - Documentation",
|
||||
]
|
||||
security:
|
||||
name: "type: security"
|
||||
color: "b60205" # Dark Red
|
||||
description: "Security vulnerability or concern"
|
||||
aliases:
|
||||
[
|
||||
"Kind/Security",
|
||||
"security",
|
||||
"Type: Security",
|
||||
"Type/Security",
|
||||
"Kind - Security",
|
||||
]
|
||||
testing:
|
||||
name: "type: testing"
|
||||
color: "0e8a16" # Green
|
||||
description: "Related to testing"
|
||||
aliases:
|
||||
[
|
||||
"Kind/Testing",
|
||||
"testing",
|
||||
"tests",
|
||||
"Type: Testing",
|
||||
"Type/Testing",
|
||||
"Kind - Testing",
|
||||
]
|
||||
status:
|
||||
ai_approved: "ai-approved"
|
||||
ai_changes_required: "ai-changes-required"
|
||||
ai_reviewed: "ai-reviewed"
|
||||
ai_approved:
|
||||
name: "ai-approved"
|
||||
color: "28a745" # Green
|
||||
description: "AI review approved this PR"
|
||||
aliases:
|
||||
[
|
||||
"Status - Approved",
|
||||
"approved",
|
||||
"Status/Approved",
|
||||
"Status - AI Approved",
|
||||
]
|
||||
ai_changes_required:
|
||||
name: "ai-changes-required"
|
||||
color: "d73a4a" # Red
|
||||
description: "AI review found issues requiring changes"
|
||||
aliases:
|
||||
[
|
||||
"Status - Changes Required",
|
||||
"changes-required",
|
||||
"Status/Changes Required",
|
||||
"Status - AI Changes Required",
|
||||
]
|
||||
ai_reviewed:
|
||||
name: "ai-reviewed"
|
||||
color: "1d76db" # Blue
|
||||
description: "This issue/PR has been reviewed by AI"
|
||||
aliases:
|
||||
[
|
||||
"Reviewed - Confirmed",
|
||||
"reviewed",
|
||||
"Status/Reviewed",
|
||||
"Reviewed/Confirmed",
|
||||
"Status - Reviewed",
|
||||
]
|
||||
|
||||
# Label schema detection patterns
|
||||
# Used by setup-labels command to detect existing naming conventions
|
||||
label_patterns:
|
||||
# Detect prefix-based naming (e.g., Kind/Bug, Type/Feature)
|
||||
prefix_slash: "^(Kind|Type|Category)/(.+)$"
|
||||
# Detect dash-separated naming (e.g., Priority - High, Status - Blocked)
|
||||
prefix_dash: "^(Priority|Status|Reviewed) - (.+)$"
|
||||
# Detect colon-separated naming (e.g., type: bug, priority: high)
|
||||
colon: "^(type|priority|status): (.+)$"
|
||||
|
||||
# Security scanning rules
|
||||
security:
|
||||
|
||||
Reference in New Issue
Block a user