Skip to content

Main dev - #1

Merged
ShuhaoZhangTony merged 8 commits into
mainfrom
main-dev
Jan 3, 2026
Merged

ShuhaoZhangTony merged 8 commits into
mainfrom
main-dev

Conversation

@ShuhaoZhangTony

Copy link
Copy Markdown
Contributor

No description provided.

- Add list command with filtering (state, label, assignee, milestone, author)
- Add sorting and limit options
- Implement config.json for default settings (priority: config.json < .env < env vars < params)
- Auto-load .env files from multiple locations
- Add python-dotenv dependency
- Move documentation files to docs/ directory
- Fix linting issues

Tested with 1304 SAGE issues successfully
- Update import from sage.tools.dev.issues to sage_github
- Team command now works correctly
- Successfully loads team info (26 members from 3 teams)
- Add IssuesExporter class with three formats:
  - CSV: Full issue data with all fields
  - JSON: Structured data for API integration
  - Markdown: Three templates (default/roadmap/report)
- Add export_issues() method to GitHubManager
- Add export CLI command with filtering support
- Update documentation to mark export as completed
- Add B008 to ruff ignore (required for Typer)

Tests:
- CSV export: 128 open issues → 11.08 KB
- JSON export: 209 closed bugs → 63.34 KB
- Markdown roadmap: 128 open issues → 7.76 KB

All formats tested and working correctly.
- Add BatchOperations class for bulk issue management:
  - batch-close: Close multiple issues at once
  - batch-label: Add/remove labels in bulk
  - batch-assign: Assign issues to users
  - batch-milestone: Set milestones for multiple issues

- Key Features:
  - Dry-run mode (--dry-run) to preview changes
  - Confirmation prompts (skip with --yes)
  - Rich preview tables (first 20 issues)
  - Progress bars for operations
  - Full filtering support (reuses list filters)
  - Direct GitHub REST API integration

- Implementation:
  - BatchOperations class (454 lines) with requests
  - 5 batch methods in manager.py
  - 4 CLI commands with comprehensive help

- Testing:
  - Dry-run tested with 128 SAGE open issues
  - Preview table displays correctly
  - All filters working

- Documentation:
  - Updated copilot-instructions.md (mark batch as implemented)
  - Updated MISSING_FEATURES.md (status 🟢 Completed)
  - Created IMPLEMENTATION_PROGRESS.md (project summary)
- Remove deprecated settings:
  - ruff.lint.run (auto-detected now)
  - ruff.format.args (use ruff.toml)
  - ruff.lint.args (use ruff.toml)

- Add new settings:
  - ruff.configurationPreference: filesystemFirst (auto-detect ruff.toml)
  - ruff.path: use 'ruff' command

References:
- Migration guide: https://docs.astral.sh/ruff/editors/migration/
- Discussion: astral-sh/ruff#15991
AI Features (Task 4):
- Add AIHelper class for AI-powered analysis
  - summarize: Generate AI summaries (OpenAI/Claude API)
  - detect-duplicates: Find duplicates using text similarity
  - suggest-labels: Recommend labels based on keywords
- Three new CLI commands:
  - github-manager summarize <issue>
  - github-manager detect-duplicates
  - github-manager suggest-labels <issue>
- Silent mode for non-API operations
- Rich table output for duplicates
- Tested: 592 duplicate pairs found in 1304 SAGE issues

Command Naming Consistency (Task 5):
- Rename 'stats' to 'analytics' for consistency
- Keep 'stats' as hidden alias with deprecation warning
- Update all documentation (README, FAQ, QUICK_START, etc.)
- Add comprehensive docstring for analytics command

Documentation:
- Updated MISSING_FEATURES.md (Tasks 4 & 5 completed)
- Fixed references across all docs
- Added usage examples

Files Changed:
- src/sage_github/helpers/ai_helper.py (new, 308 lines)
- src/sage_github/manager.py (3 new AI methods)
- src/sage_github/cli.py (3 AI commands, analytics rename)
- All documentation files updated
@ShuhaoZhangTony ShuhaoZhangTony self-assigned this Jan 3, 2026
Copilot AI review requested due to automatic review settings January 3, 2026 15:53
@ShuhaoZhangTony
ShuhaoZhangTony merged commit a252c75 into main Jan 3, 2026
2 of 7 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements major new functionality for the sage-github-manager project, including list/filter, export, batch operations, and AI-powered features. It also introduces configuration improvements with .env file support and updates documentation to reflect the new capabilities.

Key Changes:

  • Adds multi-layer configuration system with .env file support via python-dotenv
  • Implements issue filtering, exporting (CSV/JSON/Markdown), and batch operations (close, label, assign, milestone)
  • Adds AI helper functionality for issue summarization, duplicate detection, and label suggestions
  • Reorganizes imports alphabetically across multiple files for consistency
  • Updates documentation and examples to reflect new features

Reviewed changes

Copilot reviewed 37 out of 45 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/sage_github/config.py Adds .env loading and config.json support with multi-layer priority system
src/sage_github/manager.py Adds methods for list_issues, export_issues, batch operations, and AI features
src/sage_github/cli.py Implements new CLI commands: list, export, batch-*, summarize, detect-duplicates, suggest-labels
src/sage_github/helpers/filter_issues.py New module: flexible issue filtering with multiple criteria
src/sage_github/helpers/export_issues.py New module: export to CSV/JSON/Markdown with three templates
src/sage_github/helpers/batch_operations.py New module: batch GitHub API operations with dry-run and confirmation
src/sage_github/helpers/ai_helper.py New module: AI-powered analysis using OpenAI/Claude APIs
pyproject.toml Adds python-dotenv dependency
ruff.toml Adds B008 to ignore list for Typer compatibility
docs/* Updates all documentation to reflect new commands and features
tests/* Import reorganization for consistency

Comment thread src/sage_github/cli.py
@app.command("list")
def list_issues(
state: str = "open",
label: list[str] | None = None,

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

Typer default argument warning: Using mutable default list[str] | None = None is correct, but the actual parameter label should use typer.Option(None) instead of None as the direct default to avoid B008 warnings. While ruff.toml ignores B008 globally, it's better to use Typer's patterns correctly.

Change to: label: list[str] = typer.Option([], "--label", "-l", help="...")
This makes the intent clearer and follows Typer best practices.

Suggested change
label: list[str] | None = None,
label: list[str] = typer.Option(
[],
"--label",
"-l",
help="按标签过滤,可多次使用此选项指定多个标签",
),

Copilot uses AI. Check for mistakes.
Comment thread src/sage_github/config.py
Comment on lines +148 to +154
try:
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
# print(f"✅ 已加载配置: {config_path}")
return config
except Exception as e:
print(f"⚠️ 加载配置文件失败 {config_path}: {e}")

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

This violates the project's critical "NO FALLBACK LOGIC" principle. When config file loading fails (line 153), the code catches the exception and prints a warning, then continues by returning default config. This silently hides configuration errors.

According to the project guidelines, configuration loading should fail fast and loud. If a config.json file exists but is malformed, the exception should propagate to alert the user immediately rather than falling back to defaults.

Copilot generated this review using guidance from repository custom instructions.
try:
self._update_issue(issue["number"], state="closed")
success_count += 1
except Exception as e:

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

Broad exception catching without proper error handling. This catches all exceptions (including KeyboardInterrupt, SystemExit) when updating an issue. The error is printed but the operation continues, which could hide serious issues like network failures or authentication problems.

Consider catching only specific exceptions (requests.RequestException, KeyError for missing 'number' key) and potentially stopping after repeated failures rather than continuing through all issues.

Suggested change
except Exception as e:
except (requests.RequestException, KeyError) as e:

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +428
for issue in issues:
try:
# 获取现有标签
existing_labels = [label["name"] for label in issue.get("labels", [])]
# 合并标签(去重)
new_labels = list(set(existing_labels + labels))
self._update_issue(issue["number"], labels=new_labels)
success_count += 1
except Exception as e:
console.print(f"❌ [red]为 #{issue['number']} 添加标签失败: {e}[/red]")
failed_count += 1
progress.advance(task)

console.print(f"\n✅ [green]成功为 {success_count} 个 Issues 添加标签[/green]")
if failed_count > 0:
console.print(f"❌ [red]失败 {failed_count} 个[/red]")

return {
"total": len(issues),
"success": success_count,
"failed": failed_count,
"skipped": 0,
}

def remove_labels(
self,
issues: list[dict[str, Any]],
labels: list[str],
dry_run: bool = False,
auto_confirm: bool = False,
) -> dict[str, Any]:
"""批量移除标签

Args:
issues: 要移除标签的 Issues 列表
labels: 要移除的标签列表
dry_run: 是否为预览模式
auto_confirm: 是否自动确认

Returns:
操作结果统计信息
"""
if not issues:
console.print("⚠️ [yellow]没有匹配的 Issues[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

if not labels:
console.print("⚠️ [yellow]请指定要移除的标签[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

# 显示预览
self._show_preview_table(issues, f"Remove labels: {', '.join(labels)}")

if dry_run:
console.print(
f"\n🔍 [yellow]预览模式: 将从 {len(issues)} 个 Issues 移除标签 {labels}[/yellow]"
)
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

if not auto_confirm:
if not Confirm.ask(f"\n❓ 确认从 {len(issues)} 个 Issues 移除标签 {labels}?"):
console.print("❌ [red]操作已取消[/red]")
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

# 执行移除标签
success_count = 0
failed_count = 0

from rich.progress import Progress, SpinnerColumn, TextColumn

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("移除标签...", total=len(issues))

for issue in issues:
try:
# 获取现有标签并移除指定标签
existing_labels = [label["name"] for label in issue.get("labels", [])]
new_labels = [label for label in existing_labels if label not in labels]
self._update_issue(issue["number"], labels=new_labels)
success_count += 1
except Exception as e:
console.print(f"❌ [red]从 #{issue['number']} 移除标签失败: {e}[/red]")
failed_count += 1
progress.advance(task)

console.print(f"\n✅ [green]成功从 {success_count} 个 Issues 移除标签[/green]")
if failed_count > 0:
console.print(f"❌ [red]失败 {failed_count} 个[/red]")

return {
"total": len(issues),
"success": success_count,
"failed": failed_count,
"skipped": 0,
}

def assign_issues(
self,
issues: list[dict[str, Any]],
assignees: list[str],
dry_run: bool = False,
auto_confirm: bool = False,
) -> dict[str, Any]:
"""批量分配 Issues

Args:
issues: 要分配的 Issues 列表
assignees: 负责人列表
dry_run: 是否为预览模式
auto_confirm: 是否自动确认

Returns:
操作结果统计信息
"""
if not issues:
console.print("⚠️ [yellow]没有匹配的 Issues[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

if not assignees:
console.print("⚠️ [yellow]请指定负责人[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

# 显示预览
self._show_preview_table(issues, f"Assign to: {', '.join(assignees)}")

if dry_run:
console.print(
f"\n🔍 [yellow]预览模式: 将分配 {len(issues)} 个 Issues 给 {assignees}[/yellow]"
)
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

if not auto_confirm:
if not Confirm.ask(f"\n❓ 确认分配 {len(issues)} 个 Issues 给 {assignees}?"):
console.print("❌ [red]操作已取消[/red]")
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

# 执行分配
success_count = 0
failed_count = 0

from rich.progress import Progress, SpinnerColumn, TextColumn

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("分配 Issues...", total=len(issues))

for issue in issues:
try:
self._update_issue(issue["number"], assignees=assignees)
success_count += 1
except Exception as e:
console.print(f"❌ [red]分配 #{issue['number']} 失败: {e}[/red]")
failed_count += 1
progress.advance(task)

console.print(f"\n✅ [green]成功分配 {success_count} 个 Issues[/green]")
if failed_count > 0:
console.print(f"❌ [red]失败 {failed_count} 个[/red]")

return {
"total": len(issues),
"success": success_count,
"failed": failed_count,
"skipped": 0,
}

def set_milestone(
self,
issues: list[dict[str, Any]],
milestone: str,
dry_run: bool = False,
auto_confirm: bool = False,
) -> dict[str, Any]:
"""批量设置里程碑

Args:
issues: 要设置里程碑的 Issues 列表
milestone: 里程碑名称
dry_run: 是否为预览模式
auto_confirm: 是否自动确认

Returns:
操作结果统计信息
"""
if not issues:
console.print("⚠️ [yellow]没有匹配的 Issues[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

if not milestone:
console.print("⚠️ [yellow]请指定里程碑[/yellow]")
return {"total": 0, "success": 0, "failed": 0, "skipped": 0}

# 显示预览
self._show_preview_table(issues, f"Set milestone: {milestone}")

if dry_run:
console.print(
f"\n🔍 [yellow]预览模式: 将为 {len(issues)} 个 Issues 设置里程碑 '{milestone}'[/yellow]"
)
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

if not auto_confirm:
if not Confirm.ask(f"\n❓ 确认为 {len(issues)} 个 Issues 设置里程碑 '{milestone}'?"):
console.print("❌ [red]操作已取消[/red]")
return {"total": len(issues), "success": 0, "failed": 0, "skipped": len(issues)}

# 先获取里程碑 ID
try:
milestone_id = self._get_milestone_id(milestone)
if milestone_id is None:
console.print(f"❌ [red]找不到里程碑 '{milestone}'[/red]")
return {"total": len(issues), "success": 0, "failed": len(issues), "skipped": 0}
except Exception as e:
console.print(f"❌ [red]获取里程碑失败: {e}[/red]")
return {"total": len(issues), "success": 0, "failed": len(issues), "skipped": 0}

# 执行设置里程碑
success_count = 0
failed_count = 0

from rich.progress import Progress, SpinnerColumn, TextColumn

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("设置里程碑...", total=len(issues))

for issue in issues:
try:
self._update_issue(issue["number"], milestone=milestone_id)
success_count += 1
except Exception as e:
console.print(f"❌ [red]为 #{issue['number']} 设置里程碑失败: {e}[/red]")

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

This pattern of broad exception catching appears throughout the batch operations file (lines 195, 271, 344, 427). All these locations catch Exception broadly without distinguishing between different error types. This makes it difficult to identify systematic issues (like authentication failure or rate limiting) versus individual issue problems.

Consider refactoring to:

  1. Catch specific exceptions (requests.RequestException, KeyError)
  2. Detect and handle rate limiting specifically
  3. Stop after N consecutive failures to avoid hammering the API
  4. Return more detailed error information in the result dict

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +126
except Exception as e:
print(f"❌ CSV导出失败: {e}")
return False

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

Broad exception catching that masks the actual error. The function catches all exceptions (including KeyboardInterrupt, SystemExit) and only prints a generic message. This makes debugging difficult when exports fail.

Consider:

  1. Catching specific exceptions (IOError, OSError, csv.Error)
  2. Letting other exceptions propagate
  3. Including more context in the error message (e.g., file path, number of issues being exported)

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +183
except Exception as e:
print(f"❌ JSON导出失败: {e}")
return False

def export_to_markdown(self, output_path: Path, template: str = "default") -> bool:
"""
导出为Markdown格式

Args:
output_path: 输出文件路径
template: 模板类型 (default/roadmap/report)

Returns:
是否成功
"""
if not self.issues:
print("⚠️ 没有Issues需要导出")
return False

try:
if template == "roadmap":
content = self._generate_roadmap_markdown()
elif template == "report":
content = self._generate_report_markdown()
else:
content = self._generate_default_markdown()

with open(output_path, "w", encoding="utf-8") as f:
f.write(content)

return True
except Exception as e:
print(f"❌ Markdown导出失败: {e}")

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

Same broad exception catching issue as line 124-126. This pattern appears in export_to_json (152), export_to_markdown (183), and is a systemic problem throughout the export module.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +70
except ImportError:
console.print(f"⚠️ [yellow]{self.api_provider} 库未安装[/yellow]")
if self.api_provider == "openai":
console.print("💡 安装: pip install openai")
else:
console.print("💡 安装: pip install anthropic")

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

The instructions suggest installing packages with pip install, which violates the project's "ALWAYS USE pyproject.toml" principle. Dependencies should be declared in pyproject.toml under optional-dependencies (e.g., an "ai" extras group) rather than instructing users to manually install them.

Suggested fix: Add to pyproject.toml:

[project.optional-dependencies]
ai = ["openai>=1.0.0", "anthropic>=0.7.0"]

Then update the message to: "Install with: pip install sage-github-manager[ai]"

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +107 to +159
try:
if self.api_provider == "openai":
return self._summarize_with_openai(prompt, max_length)
elif self.api_provider == "claude":
return self._summarize_with_claude(prompt, max_length)
except Exception as e:
console.print(f"❌ [red]生成摘要失败: {e}[/red]")
return None

return None

def _summarize_with_openai(self, prompt: str, max_length: int) -> str | None:
"""使用 OpenAI 生成摘要"""
try:
import openai

client = openai.OpenAI(api_key=self.api_key)

response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "你是一个专业的技术文档助手,擅长总结 GitHub Issues。",
},
{"role": "user", "content": prompt},
],
max_tokens=max_length,
temperature=0.3,
)

return response.choices[0].message.content.strip()
except Exception as e:
console.print(f"❌ [red]OpenAI API 调用失败: {e}[/red]")
return None

def _summarize_with_claude(self, prompt: str, max_length: int) -> str | None:
"""使用 Claude 生成摘要"""
try:
import anthropic

client = anthropic.Anthropic(api_key=self.api_key)

message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=max_length,
messages=[{"role": "user", "content": prompt}],
)

return message.content[0].text.strip()
except Exception as e:
console.print(f"❌ [red]Claude API 调用失败: {e}[/red]")
return None

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

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

Broad exception catching in AI API calls. Lines 112, 139, and 157 all catch Exception broadly. These should catch specific API exceptions (openai.APIError, anthropic.APIError, etc.) to distinguish between transient network issues, authentication failures, rate limiting, and other errors.

This makes debugging difficult and prevents implementing appropriate retry logic for different error types.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants