mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
fix: address PR #173 security and code quality feedback
Security fixes:
- Enhanced branch name sanitization for cross-platform filesystem safety
(handles Windows-invalid chars, reserved names, path length limits)
- Added branch name validation in pr-info.ts to prevent command injection
- Sanitized prUrl in kanban-card to only allow http/https URLs
Code quality improvements:
- Fixed placeholder issue where {owner}/{repo} was passed literally to gh api
- Replaced async forEach with Promise.all for proper async handling
- Display PR number extracted from URL in kanban cards
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -421,69 +421,21 @@ export function BoardView() {
|
||||
// Handler for addressing PR comments - creates a feature and starts it automatically
|
||||
const handleAddressPRComments = useCallback(
|
||||
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
||||
// If comments are empty, fetch them from GitHub
|
||||
let fullPRInfo = prInfo;
|
||||
if (prInfo.comments.length === 0 && prInfo.reviewComments.length === 0) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.worktree?.getPRInfo) {
|
||||
const result = await api.worktree.getPRInfo(
|
||||
worktree.path,
|
||||
worktree.branch
|
||||
);
|
||||
if (
|
||||
result.success &&
|
||||
result.result?.hasPR &&
|
||||
result.result.prInfo
|
||||
) {
|
||||
fullPRInfo = result.result.prInfo;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Board] Failed to fetch PR comments:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Format PR comments into a feature description
|
||||
const allComments = [
|
||||
...fullPRInfo.comments.map((c) => ({
|
||||
...c,
|
||||
type: "comment" as const,
|
||||
})),
|
||||
...fullPRInfo.reviewComments.map((c) => ({
|
||||
...c,
|
||||
type: "review" as const,
|
||||
})),
|
||||
].sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
|
||||
let description = `Address PR #${fullPRInfo.number} feedback: "${fullPRInfo.title}"\n\n`;
|
||||
description += `PR URL: ${fullPRInfo.url}\n\n`;
|
||||
|
||||
if (allComments.length === 0) {
|
||||
description += `No comments found on this PR yet. Check the PR for any new feedback.\n`;
|
||||
} else {
|
||||
description += `## Feedback to address:\n\n`;
|
||||
for (const comment of allComments) {
|
||||
if (comment.type === "review" && comment.path) {
|
||||
description += `### ${comment.path}${comment.line ? `:${comment.line}` : ""}\n`;
|
||||
}
|
||||
description += `**@${comment.author}:**\n${comment.body}\n\n`;
|
||||
}
|
||||
}
|
||||
// Use a simple prompt that instructs the agent to read and address PR feedback
|
||||
// The agent will fetch the PR comments directly, which is more reliable and up-to-date
|
||||
const prNumber = prInfo.number;
|
||||
const description = `Read the review requests on PR #${prNumber} and address any feedback the best you can.`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
category: "PR Review",
|
||||
description: description.trim(),
|
||||
description,
|
||||
steps: [],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: "sonnet" as const,
|
||||
thinkingLevel: "medium" as const,
|
||||
model: "opus" as const,
|
||||
thinkingLevel: "none" as const,
|
||||
branchName: worktree.branch,
|
||||
priority: 1, // High priority for PR feedback
|
||||
planningMode: "skip" as const,
|
||||
@@ -500,7 +452,7 @@ export function BoardView() {
|
||||
(f) =>
|
||||
f.branchName === worktree.branch &&
|
||||
f.status === "backlog" &&
|
||||
f.description.includes(`PR #${fullPRInfo.number}`)
|
||||
f.description.includes(`PR #${prNumber}`)
|
||||
);
|
||||
|
||||
if (newFeature) {
|
||||
@@ -1255,12 +1207,19 @@ export function BoardView() {
|
||||
// If a PR was created and we have the worktree branch, update all features on that branch with the PR URL
|
||||
if (prUrl && selectedWorktreeForAction?.branch) {
|
||||
const branchName = selectedWorktreeForAction.branch;
|
||||
hookFeatures
|
||||
.filter((f) => f.branchName === branchName)
|
||||
.forEach((feature) => {
|
||||
updateFeature(feature.id, { prUrl });
|
||||
persistFeatureUpdate(feature.id, { prUrl });
|
||||
});
|
||||
const featuresToUpdate = hookFeatures.filter((f) => f.branchName === branchName);
|
||||
|
||||
// Update local state synchronously
|
||||
featuresToUpdate.forEach((feature) => {
|
||||
updateFeature(feature.id, { prUrl });
|
||||
});
|
||||
|
||||
// Persist changes asynchronously and in parallel
|
||||
Promise.all(
|
||||
featuresToUpdate.map((feature) =>
|
||||
persistFeatureUpdate(feature.id, { prUrl })
|
||||
)
|
||||
).catch(console.error);
|
||||
}
|
||||
setWorktreeRefreshKey((k) => k + 1);
|
||||
setSelectedWorktreeForAction(null);
|
||||
|
||||
@@ -699,24 +699,30 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* PR URL Display */}
|
||||
{feature.prUrl && (
|
||||
<div className="mb-2">
|
||||
<a
|
||||
href={feature.prUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] text-purple-500 hover:text-purple-400 transition-colors"
|
||||
title={feature.prUrl}
|
||||
data-testid={`pr-url-${feature.id}`}
|
||||
>
|
||||
<GitPullRequest className="w-3 h-3 shrink-0" />
|
||||
<span className="truncate max-w-[150px]">Pull Request</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{typeof feature.prUrl === "string" &&
|
||||
/^https?:\/\//i.test(feature.prUrl) && (() => {
|
||||
const prNumber = feature.prUrl.split('/').pop();
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<a
|
||||
href={feature.prUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] text-purple-500 hover:text-purple-400 transition-colors"
|
||||
title={feature.prUrl}
|
||||
data-testid={`pr-url-${feature.id}`}
|
||||
>
|
||||
<GitPullRequest className="w-3 h-3 shrink-0" />
|
||||
<span className="truncate max-w-[150px]">
|
||||
{prNumber ? `Pull Request #${prNumber}` : 'Pull Request'}
|
||||
</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Steps Preview */}
|
||||
{showSteps && feature.steps && feature.steps.length > 0 && (
|
||||
|
||||
@@ -29,12 +29,8 @@ interface CreatePRDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
worktree: WorktreeInfo | null;
|
||||
<<<<<<< Updated upstream
|
||||
onCreated: (prUrl?: string) => void;
|
||||
=======
|
||||
projectPath: string | null;
|
||||
onCreated: () => void;
|
||||
>>>>>>> Stashed changes
|
||||
onCreated: (prUrl?: string) => void;
|
||||
}
|
||||
|
||||
export function CreatePRDialog({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
<<<<<<< Updated upstream
|
||||
import { RefreshCw, Globe, Loader2, CircleDot } from "lucide-react";
|
||||
import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -9,12 +8,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { WorktreeInfo, BranchInfo, DevServerInfo } from "../types";
|
||||
=======
|
||||
import { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
|
||||
>>>>>>> Stashed changes
|
||||
import { BranchSwitchDropdown } from "./branch-switch-dropdown";
|
||||
import { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
|
||||
|
||||
@@ -95,7 +89,6 @@ export function WorktreeTab({
|
||||
onStopDevServer,
|
||||
onOpenDevServerUrl,
|
||||
}: WorktreeTabProps) {
|
||||
<<<<<<< Updated upstream
|
||||
// Determine border color based on state:
|
||||
// - Running features: cyan border (high visibility, indicates active work)
|
||||
// - Uncommitted changes: amber border (warning state, needs attention)
|
||||
@@ -111,7 +104,7 @@ export function WorktreeTab({
|
||||
};
|
||||
|
||||
const borderClasses = getBorderClasses();
|
||||
=======
|
||||
|
||||
let prBadge: JSX.Element | null = null;
|
||||
if (worktree.pr) {
|
||||
const prState = worktree.pr.state?.toLowerCase() ?? "open";
|
||||
@@ -197,7 +190,6 @@ export function WorktreeTab({
|
||||
</button>
|
||||
);
|
||||
}
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center rounded-md", borderClasses)}>
|
||||
@@ -225,7 +217,6 @@ export function WorktreeTab({
|
||||
{cardCount}
|
||||
</span>
|
||||
)}
|
||||
<<<<<<< Updated upstream
|
||||
{hasChanges && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -246,9 +237,7 @@ export function WorktreeTab({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
=======
|
||||
{prBadge}
|
||||
>>>>>>> Stashed changes
|
||||
</Button>
|
||||
<BranchSwitchDropdown
|
||||
worktree={worktree}
|
||||
@@ -292,7 +281,6 @@ export function WorktreeTab({
|
||||
{cardCount}
|
||||
</span>
|
||||
)}
|
||||
<<<<<<< Updated upstream
|
||||
{hasChanges && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -313,9 +301,7 @@ export function WorktreeTab({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
=======
|
||||
{prBadge}
|
||||
>>>>>>> Stashed changes
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user