fix(github): fix investigation posting to GitHub with proper error handling

Fixed critical bug where posting investigation findings to GitHub failed
due to gh CLI command incompatibility. The 'gh issue comment' command
doesn't support the --json flag, causing all posting attempts to fail.

Changes:
- Switch from 'gh issue comment' to 'gh api' with REST endpoint
- Remove _add_repo_flag call (gh api uses repo in endpoint URL)
- Return comment ID instead of None from _post_issue_comment
- Add comprehensive error handling with user-friendly messages
- Add toast notifications for success/failure feedback
- Add i18n translations for posting status messages

Error handling improvements:
- Detect common gh CLI failures (auth, rate limit, permissions)
- Output JSON-formatted errors for frontend parsing
- Show clear error messages to users

Fixes issue where clicking "Post Findings" would silently fail or show
cryptic "unknown flag: --json" error.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-16 14:40:36 +01:00
co-authored by Claude Sonnet 4.5
parent c0102c3d05
commit a7a85730ab
7 changed files with 146 additions and 38 deletions
+75 -4
View File
@@ -539,17 +539,88 @@ class GHClient:
logger.debug("Raw stdout (truncated): %s", result.stdout[:200])
return {}
async def issue_comment(self, issue_number: int, body: str) -> None:
async def issue_comment(self, issue_number: int, body: str) -> int:
"""
Post a comment to an issue.
Args:
issue_number: Issue number
body: Comment body
Returns:
The ID of the created comment
Raises:
GHCommandError: If the gh CLI command fails
ValueError: If the comment ID is not returned
"""
args = ["issue", "comment", str(issue_number), "--body", body]
args = self._add_repo_flag(args)
await self.run(args)
# Use GitHub API directly via 'gh api' to post the comment
# This allows us to get JSON response with the comment ID
# Note: gh issue comment doesn't support --json flag, but gh api does
endpoint = f"repos/{{owner}}/{{repo}}/issues/{issue_number}/comments"
args = [
"api",
"--method", "POST",
endpoint,
"-f", f"body={body}",
"--jq", ".id",
]
# Note: gh api doesn't use -R flag, the repo is already in the endpoint URL
# Only add -R if we have a custom repo configured (not the default from git)
if self.repo:
# For gh api, we need to use --hostname to specify a different repo
# But since the endpoint already has {{owner}}/{{repo}}, it will be
# expanded correctly by gh CLI using the default git remote
pass
result = await self.run(args, raise_on_error=False)
# Check for gh CLI errors
if result.returncode != 0:
stderr_lower = result.stderr.lower()
error_msg = result.stderr.strip() or f"gh CLI failed with exit code {result.returncode}"
# Provide user-friendly error messages for common failure modes
if "401" in result.stderr or "unauthenticated" in stderr_lower or "not logged in" in stderr_lower:
raise GHCommandError(
"GitHub authentication failed. Please run 'gh auth login' to authenticate."
)
elif "403" in result.stderr or "429" in result.stderr or "rate limit" in stderr_lower:
raise GHCommandError(
"GitHub API rate limit exceeded. Please wait a few minutes before trying again."
)
elif "404" in result.stderr or "not found" in stderr_lower:
if self.repo:
raise GHCommandError(
f"Repository or issue not found. Please verify that the repository '{self.repo}' exists and you have access to it."
)
else:
raise GHCommandError(
f"Issue #{issue_number} not found. Please verify that the issue exists in the repository."
)
elif "permission" in stderr_lower or "forbidden" in stderr_lower:
raise GHCommandError(
"You do not have permission to comment on this issue. Please ensure you have write access to the repository."
)
# Generic error with stderr output
raise GHCommandError(f"Failed to post comment to GitHub: {error_msg}")
# Parse the comment ID from the output (gh api --jq .id returns just the ID)
try:
stdout = result.stdout.strip()
if not stdout:
raise ValueError("Empty response from GitHub API")
comment_id = int(stdout)
if comment_id:
return comment_id
else:
raise ValueError(f"Invalid comment ID returned: {stdout}")
except (ValueError, json.JSONDecodeError) as e:
logger.error("Failed to parse comment ID response for issue #%d: %s", issue_number, e)
logger.debug("Raw stdout (first 500 chars): %s", result.stdout[:500])
logger.debug("Raw stderr (first 500 chars): %s", result.stderr[:500])
raise ValueError(f"Failed to parse GitHub response: {str(e)}")
async def issue_comments(self, issue_number: int) -> list[dict]:
"""
+3 -3
View File
@@ -262,9 +262,9 @@ class GitHubOrchestrator:
event=event.lower(),
)
async def _post_issue_comment(self, issue_number: int, body: str) -> None:
"""Post a comment to an issue."""
await self.gh_client.issue_comment(issue_number, body)
async def _post_issue_comment(self, issue_number: int, body: str) -> int:
"""Post a comment to an issue and return the comment ID."""
return await self.gh_client.issue_comment(issue_number, body)
async def _add_issue_labels(self, issue_number: int, labels: list[str]) -> None:
"""Add labels to an issue."""
+47 -27
View File
@@ -386,37 +386,57 @@ async def cmd_post_investigation(args) -> int:
from services.investigation_persistence import load_investigation_report
from services.investigation_report_builder import build_github_comment
config = get_config(args)
orchestrator = GitHubOrchestrator(
project_dir=args.project,
config=config,
progress_callback=print_progress,
)
def output_error(error_message: str) -> None:
"""Output an error in JSON format for the Electron frontend to parse."""
safe_print("\nJSON Output")
safe_print(f"{'=' * 60}")
safe_print(json.dumps({"success": False, "error": error_message}))
# Load investigation report from .auto-claude/issues/{issueNumber}/investigation_report.json
report = load_investigation_report(args.project, args.issue_number)
if report is None:
safe_print(
f"Error: No investigation report found for issue #{args.issue_number}. "
"Run investigation first."
try:
config = get_config(args)
orchestrator = GitHubOrchestrator(
project_dir=args.project,
config=config,
progress_callback=print_progress,
)
# Load investigation report from .auto-claude/issues/{issueNumber}/investigation_report.json
report = load_investigation_report(args.project, args.issue_number)
if report is None:
error_msg = (
f"No investigation report found for issue #{args.issue_number}. "
f"Please run the investigation first."
)
safe_print(f"Error: {error_msg}")
output_error(error_msg)
return 1
# Build the GitHub comment from the report
comment_body = build_github_comment(report)
# Post it to GitHub
try:
comment_id = await orchestrator._post_issue_comment(args.issue_number, comment_body)
except Exception as e:
error_msg = f"Failed to post comment to GitHub: {str(e)}"
safe_print(f"Error: {error_msg}")
output_error(error_msg)
return 1
safe_print(f"Posted investigation results to issue #{args.issue_number}")
# Output JSON for the Electron frontend to parse
safe_print("\nJSON Output")
safe_print(f"{'=' * 60}")
safe_print(json.dumps({"success": True, "commentId": comment_id}))
return 0
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
safe_print(f"Error: {error_msg}")
output_error(error_msg)
return 1
# Build the GitHub comment from the report
comment_body = build_github_comment(report)
# Post it to GitHub
await orchestrator._post_issue_comment(args.issue_number, comment_body)
safe_print(f"Posted investigation results to issue #{args.issue_number}")
# Output JSON for the Electron frontend to parse
safe_print("\nJSON Output")
safe_print(f"{'=' * 60}")
safe_print(json.dumps({"success": True, "issueNumber": args.issue_number}))
return 0
async def cmd_enrich(args) -> int:
"""Enrich a single issue with deep AI analysis."""
+3 -1
View File
@@ -254,7 +254,7 @@ class MockGitHubClient:
raise Exception(f"Issue #{issue_number} not found")
return self.issues[issue_number]
async def issue_comment(self, issue_number: int, body: str) -> None:
async def issue_comment(self, issue_number: int, body: str) -> int:
self._log_call("issue_comment", issue_number=issue_number)
self.posted_comments.append(
{
@@ -262,6 +262,8 @@ class MockGitHubClient:
"body": body,
}
)
# Return a fake comment ID for testing
return 123456 + issue_number
async def issue_add_labels(self, issue_number: int, labels: list[str]) -> None:
self._log_call("issue_add_labels", issue_number=issue_number, labels=labels)
@@ -45,11 +45,13 @@ import {
AlertDialogCancel,
} from "./ui/alert-dialog";
import { useMutationStore } from "../stores/github/mutation-store";
import { useToast } from "../hooks/use-toast";
import type { GitHubIssue, InvestigationState, InvestigationDismissReason, SuggestedLabel } from "../../shared/types";
import type { GitHubIssuesProps } from "./github-issues/types";
export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesProps) {
const { t } = useTranslation("common");
const { toast } = useToast();
const projects = useProjectStore((state) => state.projects);
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
const selectedProject = projects.find((p) => p.id === selectedProjectId);
@@ -522,8 +524,17 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
// Track that we posted — use the comment ID if available, or a timestamp marker
const commentId = result.data?.commentId ?? Date.now();
investigationStore.setGithubCommentId(selectedProject.id, selectedIssue.number, commentId);
toast({
title: t('investigation.toast.postedToGitHub', { issueNumber: selectedIssue.number }),
});
} else {
toast({
title: t('investigation.toast.postToGitHubFailed'),
description: result?.error ?? t('errors.unknown'),
variant: 'destructive',
});
}
}, [selectedProject?.id, selectedIssue, investigationStore]);
}, [selectedProject?.id, selectedIssue, investigationStore, toast, t]);
const [isPostingToGitHub, setIsPostingToGitHub] = useState(false);
const handlePostToGitHubWrapped = useCallback(async () => {
@@ -1213,7 +1213,9 @@
},
"toast": {
"investigationComplete": "Investigation complete for Issue #{{issueNumber}}",
"investigationFailed": "Investigation failed for Issue #{{issueNumber}}"
"investigationFailed": "Investigation failed for Issue #{{issueNumber}}",
"postedToGitHub": "Successfully posted findings to Issue #{{issueNumber}}",
"postToGitHubFailed": "Failed to post findings to GitHub"
},
"labelConsent": {
"title": "Label Creation Notice",
@@ -1213,7 +1213,9 @@
},
"toast": {
"investigationComplete": "Investigation terminée pour l'issue #{{issueNumber}}",
"investigationFailed": "Investigation échouée pour l'issue #{{issueNumber}}"
"investigationFailed": "Investigation échouée pour l'issue #{{issueNumber}}",
"postedToGitHub": "Résultats postés avec succès sur l'issue #{{issueNumber}}",
"postToGitHubFailed": "Échec de la publication des résultats sur GitHub"
},
"labelConsent": {
"title": "Avis de création de labels",