-
Notifications
You must be signed in to change notification settings - Fork 95
feat: Add github-auto-fix-agent AgentKit #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bfb2bec
cc6857d
f953c51
0bb4565
abefcda
6c38d47
b8fc32f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| GITHUB_AUTO_FIX="YOUR_FLOW_ID" | ||
| LAMATIC_API_URL="YOUR_API_ENDPOINT" | ||
| LAMATIC_API_KEY="YOUR_API_KEY" | ||
| LAMATIC_PROJECT_ID="YOUR_PROJECT_ID" | ||
| GITHUB_TOKEN="YOUR_GITHUB_TOKEN" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
|
||
| # dependencies | ||
| /node_modules | ||
| /.pnp | ||
| .pnp.* | ||
| .yarn/* | ||
| !.yarn/patches | ||
| !.yarn/plugins | ||
| !.yarn/releases | ||
| !.yarn/versions | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| # next.js | ||
| /.next/ | ||
| /out/ | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| # misc | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # debug | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| .pnpm-debug.log* | ||
|
|
||
| # env files (can opt-in for committing if needed) | ||
| .env | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # GitHub Auto Fix Agent | ||
|
|
||
| An AI-powered agent that analyzes GitHub issues and generates code fixes with ready-to-create pull requests. | ||
|
|
||
| --- | ||
|
|
||
| ## Features | ||
|
|
||
| - Understands GitHub issues automatically | ||
| - Identifies root cause of bugs | ||
| - Generates minimal code fixes (diff-based) | ||
| - Prepares PR metadata (title, body, branch) | ||
| - One-click PR creation | ||
|
|
||
| --- | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. User provides a GitHub issue URL | ||
| 2. Lamatic flow analyzes the issue | ||
| 3. AI generates: | ||
| - Issue summary | ||
| - Root cause | ||
| - Code fix (diff) | ||
| - PR metadata | ||
| 4. User reviews and creates PR | ||
|
|
||
| --- | ||
|
|
||
| ## Flow Overview | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| A[User Input] --> B[Frontend] | ||
| B --> C[Backend API] | ||
| C --> D[Lamatic Flow] | ||
| D --> E[Analysis + Fix + PR Data] | ||
| E --> F[Frontend Preview] | ||
| F --> G[User Creates PR] | ||
| G --> H[GitHub PR Created] | ||
| ``` | ||
|
|
||
| ## Lamatic Workflow | ||
|
|
||
| <p align="center"> | ||
| <img src="./public/flows.png" alt="Lamatic Workflow" width="700"/> | ||
| </p> | ||
|
|
||
| ## Setup Locally | ||
|
|
||
| ```bash | ||
| cd kits/agentic/github-auto-fix-agent | ||
| npm install | ||
| cp .env.example .env | ||
| npm run dev | ||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,178 @@ | ||||||||||||||||||||||||||||||||||||||||||
| "use server"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import { lamaticClient } from "@/lib/lamatic-client"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Step 1: Analyze the issue and generate a fix (NO PR creation). | ||||||||||||||||||||||||||||||||||||||||||
| * Returns analysis, fix data, and PR metadata from the Lamatic flow. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function handleFixIssue(input: { | ||||||||||||||||||||||||||||||||||||||||||
| issue_url: string; | ||||||||||||||||||||||||||||||||||||||||||
| file_path?: string; | ||||||||||||||||||||||||||||||||||||||||||
| file_content?: string; | ||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| console.log("[agent] Input received", { | ||||||||||||||||||||||||||||||||||||||||||
| hasIssueUrl: Boolean(input.issue_url), | ||||||||||||||||||||||||||||||||||||||||||
| hasFilePath: Boolean(input.file_path), | ||||||||||||||||||||||||||||||||||||||||||
| hasFileContent: Boolean(input.file_content), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| const flowId = process.env.GITHUB_AUTO_FIX; | ||||||||||||||||||||||||||||||||||||||||||
| if (!flowId) throw new Error("Missing flow ID"); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
| // Run Lamatic Flow | ||||||||||||||||||||||||||||||||||||||||||
| const resData = await lamaticClient.executeFlow(flowId, input); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (resData.status !== "success" || !resData.result) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(resData.message || "Lamatic flow failed"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const result = resData.result; | ||||||||||||||||||||||||||||||||||||||||||
| const { analysis, fix, pr } = result; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| console.log("[agent] Flow output received", { | ||||||||||||||||||||||||||||||||||||||||||
| hasAnalysis: Boolean(result?.analysis), | ||||||||||||||||||||||||||||||||||||||||||
| hasFix: Boolean(result?.fix), | ||||||||||||||||||||||||||||||||||||||||||
| hasPr: Boolean(result?.pr), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||
| analysis, | ||||||||||||||||||||||||||||||||||||||||||
| fix, | ||||||||||||||||||||||||||||||||||||||||||
| pr, // branch_name, commit_message, pr_title, pr_body | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| console.error("[agent] Error:", error); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : "Unknown error", | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Step 2: Create a GitHub PR using the fix data from Step 1. | ||||||||||||||||||||||||||||||||||||||||||
| * This is called separately only when the user clicks "Create PR". | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function handleCreatePR(input: { | ||||||||||||||||||||||||||||||||||||||||||
| issue_url: string; | ||||||||||||||||||||||||||||||||||||||||||
| file_path: string; | ||||||||||||||||||||||||||||||||||||||||||
| fix: { updated_code: string }; | ||||||||||||||||||||||||||||||||||||||||||
| pr: { | ||||||||||||||||||||||||||||||||||||||||||
| branch_name: string; | ||||||||||||||||||||||||||||||||||||||||||
| commit_message: string; | ||||||||||||||||||||||||||||||||||||||||||
| pr_title: string; | ||||||||||||||||||||||||||||||||||||||||||
| pr_body: string; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const { issue_url, file_path, fix, pr } = input; | ||||||||||||||||||||||||||||||||||||||||||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Extract repo info | ||||||||||||||||||||||||||||||||||||||||||
| const match = issue_url.match(/github.com\/(.*?)\/(.*?)\/issues\/(\d+)/); | ||||||||||||||||||||||||||||||||||||||||||
| if (!match) throw new Error("Invalid GitHub issue URL"); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const [, owner, repo] = match; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Get repo default branch | ||||||||||||||||||||||||||||||||||||||||||
| const repoData = await fetch( | ||||||||||||||||||||||||||||||||||||||||||
| `https://api.github.com/repos/${owner}/${repo}`, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ).then((res) => res.json()); | ||||||||||||||||||||||||||||||||||||||||||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const baseBranch = repoData.default_branch; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Get latest commit SHA | ||||||||||||||||||||||||||||||||||||||||||
| const refData = await fetch( | ||||||||||||||||||||||||||||||||||||||||||
| `https://api.github.com/repos/${owner}/${repo}/git/ref/heads/${baseBranch}`, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ).then((res) => res.json()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const baseSha = refData.object.sha; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Create branch | ||||||||||||||||||||||||||||||||||||||||||
| await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`, { | ||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||
| ref: `refs/heads/${pr.branch_name}`, | ||||||||||||||||||||||||||||||||||||||||||
| sha: baseSha, | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Get file SHA | ||||||||||||||||||||||||||||||||||||||||||
| const fileData = await fetch( | ||||||||||||||||||||||||||||||||||||||||||
| `https://api.github.com/repos/${owner}/${repo}/contents/${file_path}`, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ).then((res) => res.json()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const fileSha = fileData.sha; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Update file on branch | ||||||||||||||||||||||||||||||||||||||||||
| await fetch( | ||||||||||||||||||||||||||||||||||||||||||
| `https://api.github.com/repos/${owner}/${repo}/contents/${file_path}`, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| method: "PUT", | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||
| message: pr.commit_message, | ||||||||||||||||||||||||||||||||||||||||||
| content: Buffer.from(fix.updated_code).toString("base64"), | ||||||||||||||||||||||||||||||||||||||||||
| branch: pr.branch_name, | ||||||||||||||||||||||||||||||||||||||||||
| sha: fileSha, | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Create PR | ||||||||||||||||||||||||||||||||||||||||||
| const prRes = await fetch( | ||||||||||||||||||||||||||||||||||||||||||
| `https://api.github.com/repos/${owner}/${repo}/pulls`, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||
| title: pr.pr_title, | ||||||||||||||||||||||||||||||||||||||||||
| head: pr.branch_name, | ||||||||||||||||||||||||||||||||||||||||||
| base: baseBranch, | ||||||||||||||||||||||||||||||||||||||||||
| body: pr.pr_body, | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const prData = await prRes.json(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||
| pr_url: prData.html_url, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+164
to
+169
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR creation response not validated before returning URL.
🐛 Proposed fix to validate PR creation const prData = await prRes.json();
+ if (!prRes.ok || !prData.html_url) {
+ throw new Error(`Failed to create PR: ${prData.message || "Unknown error"}`);
+ }
+
return {
success: true,
pr_url: prData.html_url,
analysis,
fix,
};📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| console.error("[agent] PR creation error:", error); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : "Unknown error", | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { handleCreatePR } from "@/actions/orchestrate"; | ||
|
|
||
| export async function POST(req: Request) { | ||
| try { | ||
| const body = await req.json(); | ||
|
|
||
| if (!body.issue_url || !body.file_path || !body.fix || !body.pr) { | ||
| return Response.json( | ||
| { | ||
| success: false, | ||
| error: | ||
| "issue_url, file_path, fix, and pr are all required", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
RitoG09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const result = await handleCreatePR(body); | ||
|
|
||
| return Response.json(result); | ||
| } catch (error) { | ||
| console.error("[API ERROR]", error); | ||
|
|
||
| return Response.json( | ||
| { | ||
| success: false, | ||
| error: "Internal server error", | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { handleFixIssue } from "@/actions/orchestrate"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await req.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!body.issue_url) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "issue_url is required" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await handleFixIssue(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protect this endpoint with authentication and repository scope checks.
🔐 Proposed hardening export async function POST(req: Request) {
try {
+ const internalApiToken = process.env.INTERNAL_API_TOKEN;
+ const authHeader = req.headers.get("authorization");
+ if (!internalApiToken || authHeader !== `Bearer ${internalApiToken}`) {
+ return Response.json(
+ { success: false, error: "Unauthorized" },
+ { status: 401 },
+ );
+ }
+
const body = await req.json();
- if (!body.issue_url) {
+ if (typeof body.issue_url !== "string" || body.issue_url.trim() === "") {
return Response.json(
{ success: false, error: "issue_url is required" },
{ status: 400 },
);
}
+
+ let parsedIssueUrl: URL;
+ try {
+ parsedIssueUrl = new URL(body.issue_url);
+ } catch {
+ return Response.json(
+ { success: false, error: "issue_url must be a valid URL" },
+ { status: 400 },
+ );
+ }
+ if (parsedIssueUrl.hostname !== "github.com") {
+ return Response.json(
+ { success: false, error: "Only github.com issue URLs are supported" },
+ { status: 400 },
+ );
+ }
const result = await handleFixIssue(body);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("[API ERROR]", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging raw error objects from privileged flows. Raw exceptions may include sensitive context from upstream systems. Log a request ID + sanitized metadata instead. 🛡️ Proposed safe logging pattern } catch (error) {
- console.error("[API ERROR]", error);
+ const requestId = crypto.randomUUID();
+ console.error("[API ERROR]", {
+ requestId,
+ name: error instanceof Error ? error.name : "UnknownError",
+ });
return Response.json(
{
success: false,
- error: "Internal server error",
+ error: `Internal server error (${requestId})`,
},
{ status: 500 },
);
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Internal server error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.