-
-
Notifications
You must be signed in to change notification settings - Fork 290
feat: implement design agent for ui reviews #824
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 7 commits
c083273
80a453d
5a62c4a
133a76a
a92592a
1af086b
611f089
c10da19
69b51cf
2933d4e
8b79c38
caae559
0cc12f9
07556d1
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,122 @@ | ||||||||||||||||||||
| // get diff (pr diff or git diff depending on trigger) | ||||||||||||||||||||
| // filter to design-relevant files | ||||||||||||||||||||
| // build Claude prompt with guidelines + diff | ||||||||||||||||||||
| // call Claude API | ||||||||||||||||||||
| // post review comment to GitHub | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const Anthropic = require("@anthropic-ai/sdk"); | ||||||||||||||||||||
| const { getDiff } = require("./get-diff"); | ||||||||||||||||||||
| const { filterFiles } = require("./filter-files"); | ||||||||||||||||||||
| const { buildPrompt } = require("./prompt"); | ||||||||||||||||||||
| const { postComment } = require("./post-comment"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!process.env.GITHUB_TOKEN) { | ||||||||||||||||||||
| console.error("ERROR: GITHUB_TOKEN environment variable is not set."); | ||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!process.env.GITHUB_REPOSITORY) { | ||||||||||||||||||||
| console.error("ERROR: GITHUB_REPOSITORY environment variable is not set."); | ||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Filter the full unified diff down to only hunks belonging to relevantFiles. | ||||||||||||||||||||
| * Prevents the agent from seeing or commenting on non-design files. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * @param {string} fullDiff - raw unified diff | ||||||||||||||||||||
| * @param {string[]} relevantFiles - file paths that passed filterFiles() | ||||||||||||||||||||
| * @returns {string} filtered diff containing only relevant file sections | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function filterDiffToRelevantFiles(fullDiff, relevantFiles) { | ||||||||||||||||||||
| const relevantSet = new Set(relevantFiles); | ||||||||||||||||||||
| const sections = fullDiff.split(/^(?=diff --git )/m); | ||||||||||||||||||||
| return sections | ||||||||||||||||||||
| .filter((section) => { | ||||||||||||||||||||
| const match = section.match(/^diff --git a\/(.+) b\/.+/); | ||||||||||||||||||||
| return match && relevantSet.has(match[1]); | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .join(""); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| async function run() { | ||||||||||||||||||||
| console.log("=== Keploy Design Review Agent ==="); | ||||||||||||||||||||
| console.log(`Trigger: ${process.env.GITHUB_EVENT_NAME}`); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Step 1: Get the diff | ||||||||||||||||||||
| console.log("Fetching diff..."); | ||||||||||||||||||||
| const { diff, changedFiles } = await getDiff(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!diff || diff.trim().length === 0) { | ||||||||||||||||||||
| console.log("No diff found. Nothing to review."); | ||||||||||||||||||||
| await postComment( | ||||||||||||||||||||
| "## Keploy Design Review\n\nNo changes detected in this diff. Nothing to review." | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| console.log(`Total changed files: ${changedFiles.length}`); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Step 2: Filter to design-relevant files | ||||||||||||||||||||
| const relevantFiles = filterFiles(changedFiles); | ||||||||||||||||||||
| console.log(`Design-relevant files: ${relevantFiles.length}`); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (relevantFiles.length === 0) { | ||||||||||||||||||||
| console.log("No design-relevant files changed. Skipping review."); | ||||||||||||||||||||
| await postComment( | ||||||||||||||||||||
| "## Keploy Design Review\n\n✅ No design-relevant files changed in this diff. Nothing to review." | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Step 3: Check API key — only required if we actually have files to review. | ||||||||||||||||||||
| // This prevents failures on fork PRs where secrets are unavailable but | ||||||||||||||||||||
| // there are no design files to review anyway (already handled above). | ||||||||||||||||||||
| const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; | ||||||||||||||||||||
| if (!ANTHROPIC_API_KEY) { | ||||||||||||||||||||
| console.error("ERROR: ANTHROPIC_API_KEY environment variable is not set."); | ||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||
|
||||||||||||||||||||
| console.error("ERROR: ANTHROPIC_API_KEY environment variable is not set."); | |
| process.exit(1); | |
| console.log( | |
| "ANTHROPIC_API_KEY is not set. Skipping design review and posting a comment so maintainers can rerun after configuring credentials." | |
| ); | |
| await postComment( | |
| "## Keploy Design Review\n\n⚪ Design review was skipped because `ANTHROPIC_API_KEY` is not available in this workflow run. This commonly happens on fork pull requests where secrets are not exposed. If you need a review, ask a maintainer to rerun the workflow with the required credentials." | |
| ); | |
| return; |
Copilot
AI
Apr 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You compute relevantFiles, but still pass the full unified diff into buildPrompt(...). This means the agent can flag issues in non-relevant files (including its own .github/scripts/*) whenever at least one relevant file exists. Filter the diff down to hunks for relevantFiles (or fetch per-file diffs) before building the prompt.
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The model name is hard-coded (claude-sonnet-4-6). To avoid workflow breakage when models are renamed/deprecated, make this configurable via an env var (with a sane default) and include the model name in error output when requests fail.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // filters a list of changed file paths to only those relevant for design review | ||
|
|
||
| const ALLOWED_EXTENSIONS = [ | ||
| ".css", | ||
| ".scss", | ||
| ".sass", | ||
| ".mdx", | ||
|
amaan-bhati marked this conversation as resolved.
|
||
| ".md", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".js", | ||
| ".ts", | ||
| ]; | ||
|
|
||
| const IGNORED_PATHS = [ | ||
| "node_modules/", | ||
| "build/", | ||
| ".docusaurus/", | ||
| ".github/", | ||
| "package-lock.json", | ||
| "yarn.lock", | ||
| "pnpm-lock.yaml", | ||
| "DESIGN_GUIDELINES.md", | ||
| ]; | ||
|
Comment on lines
+16
to
+25
|
||
|
|
||
| // Only review actual site source directories | ||
| const ALLOWED_PATHS = ["src/", "docs/", "versioned_docs/", "blog/", "static/"]; | ||
|
|
||
| /** | ||
| * @param {string[]} files array of file paths from the diff | ||
| * @returns {string[]} filtered file paths | ||
| */ | ||
| function filterFiles(files) { | ||
| return files.filter((file) => { | ||
| const isIgnored = IGNORED_PATHS.some((p) => file.includes(p)); | ||
| if (isIgnored) return false; | ||
|
|
||
| const hasAllowedExt = ALLOWED_EXTENSIONS.some((ext) => file.endsWith(ext)); | ||
| if (!hasAllowedExt) return false; | ||
|
|
||
| return ALLOWED_PATHS.some((p) => file.startsWith(p)); | ||
| }); | ||
| } | ||
|
|
||
| module.exports = { filterFiles }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // returns the unified diff string and list of changed files for the current GitHub event (PR or push) | ||
|
|
||
| const { execSync } = require("child_process"); | ||
| const https = require("https"); | ||
|
|
||
| const GITHUB_TOKEN = process.env.GITHUB_TOKEN; | ||
| const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; // "owner/repo" | ||
| const PR_NUMBER = process.env.PR_NUMBER; | ||
| const GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME; | ||
| const GITHUB_SHA = process.env.GITHUB_SHA; | ||
|
|
||
| // fetch the pr diff from the GitHub API, using the "diff" media type to get a unified diff string, returns raw unified diff string | ||
| function fetchPRDiff() { | ||
| return new Promise((resolve, reject) => { | ||
| const [owner, repo] = GITHUB_REPOSITORY.split("/"); | ||
| const options = { | ||
| hostname: "api.github.com", | ||
| path: `/repos/${owner}/${repo}/pulls/${PR_NUMBER}`, | ||
| headers: { | ||
| Authorization: `Bearer ${GITHUB_TOKEN}`, | ||
| Accept: "application/vnd.github.v3.diff", | ||
| "User-Agent": "keploy-design-review-agent", | ||
| }, | ||
| }; | ||
|
|
||
| https | ||
| .get(options, (res) => { | ||
| let data = ""; | ||
| res.on("data", (chunk) => (data += chunk)); | ||
| res.on("end", () => { | ||
| if (res.statusCode < 200 || res.statusCode >= 300) { | ||
| reject( | ||
| new Error( | ||
| `GitHub API returned ${res.statusCode} fetching PR diff. ` + | ||
| `Check GITHUB_TOKEN permissions and that PR_NUMBER=${PR_NUMBER} is valid. ` + | ||
| `Response: ${data.trim().slice(0, 300)}` | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| resolve(data); | ||
| }); | ||
| }) | ||
| .on("error", reject); | ||
| }); | ||
| } | ||
|
|
||
| // get diff for a push event using git. Compares HEAD to its parent (HEAD~1). | ||
| function getCommitDiff() { | ||
| try { | ||
| const diff = execSync("git diff HEAD~1 HEAD", { | ||
| encoding: "utf8", | ||
| maxBuffer: 10 * 1024 * 1024, // 10MB | ||
| }); | ||
| return diff; | ||
| } catch { | ||
| // first commit edge case, diff against empty tree | ||
| const diff = execSync( | ||
| "git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 HEAD", | ||
| { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 } | ||
| ); | ||
| return diff; | ||
| } | ||
| } | ||
|
|
||
| // for manual workflow_dispatch: diff the last commit. this is a best practice to get some diff for manual runs, but may not be perfect depending on the repo state. | ||
|
|
||
| function getManualDiff() { | ||
| return getCommitDiff(); | ||
| } | ||
|
|
||
| // main export function that returns the diff and list of changed files based on the GitHub event type (pull_request or push) | ||
|
|
||
| // diff : raw unified diff string | ||
| // changedFiles: array of file paths that changed (extracted from diff headers) | ||
|
|
||
| async function getDiff() { | ||
| let diff = ""; | ||
|
|
||
| if (GITHUB_EVENT_NAME === "pull_request") { | ||
| diff = await fetchPRDiff(); | ||
| } else if (GITHUB_EVENT_NAME === "push") { | ||
| diff = getCommitDiff(); | ||
| } else { | ||
| // workflow_dispatch or any other trigger | ||
| diff = getManualDiff(); | ||
| } | ||
|
amaan-bhati marked this conversation as resolved.
|
||
|
|
||
| // Extract unique file names from diff headers: "diff --git a/foo b/foo" | ||
| const fileMatches = [...diff.matchAll(/^diff --git a\/(.+) b\/.+$/gm)]; | ||
| const changedFiles = [...new Set(fileMatches.map((m) => m[1]))]; | ||
|
|
||
| return { diff, changedFiles }; | ||
| } | ||
|
|
||
| module.exports = { getDiff }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // posts the design review results as a github comment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // on a pr: posts a pr review comment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
amaan-bhati marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // on a push: posts a commit comment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // on a manual workflow_dispatch: posts a commit comment (best effort to get some comment for manual runs, but may not be perfect depending on repo state) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const https = require("https"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const GITHUB_TOKEN = process.env.GITHUB_TOKEN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PR_NUMBER = process.env.PR_NUMBER; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const GITHUB_SHA = process.env.GITHUB_SHA; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function githubRequest(method, path, body) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payload = JSON.stringify(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const options = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname: "api.github.com", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Accept: "application/vnd.github.v3+json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Length": Buffer.byteLength(payload), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "User-Agent": "keploy-design-review-agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = https.request(options, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("data", (chunk) => (data += chunk)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("end", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (res.statusCode >= 200 && res.statusCode < 300) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 204 No Content (DELETE) returns empty body — safe parse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(data.length > 0 ? JSON.parse(data) : {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `GitHub API error ${res.statusCode}: ${data}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.on("error", reject); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.write(payload); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.end(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Find and delete any previous design review comment on the PR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * so we don't accumulate stale comments. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function deletePreviousReviewComment(owner, repo) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const listPath = `/repos/${owner}/${repo}/issues/${PR_NUMBER}/comments`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const comments = await new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const options = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname: "api.github.com", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: listPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "GET", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${GITHUB_TOKEN}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Accept: "application/vnd.github.v3+json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "User-Agent": "keploy-design-review-agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| https | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get(options, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("data", (c) => (data += c)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("end", () => resolve(JSON.parse(data))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .on("error", reject); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+132
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |
| * Find and delete any previous design review comment on the PR | |
| * so we don't accumulate stale comments. | |
| */ | |
| async function deletePreviousReviewComment(owner, repo) { | |
| const listPath = `/repos/${owner}/${repo}/issues/${PR_NUMBER}/comments`; | |
| const comments = await new Promise((resolve, reject) => { | |
| const options = { | |
| hostname: "api.github.com", | |
| path: listPath, | |
| method: "GET", | |
| headers: { | |
| Authorization: `Bearer ${GITHUB_TOKEN}`, | |
| Accept: "application/vnd.github.v3+json", | |
| "User-Agent": "keploy-design-review-agent", | |
| }, | |
| }; | |
| https | |
| .get(options, (res) => { | |
| let data = ""; | |
| res.on("data", (c) => (data += c)); | |
| res.on("end", () => resolve(JSON.parse(data))); | |
| }) | |
| .on("error", reject); | |
| }); | |
| function parseNextLink(linkHeader) { | |
| if (!linkHeader) { | |
| return null; | |
| } | |
| const links = linkHeader.split(","); | |
| for (const link of links) { | |
| const match = link.match(/<([^>]+)>;\s*rel="([^"]+)"/); | |
| if (match && match[2] === "next") { | |
| const nextUrl = new URL(match[1]); | |
| return `${nextUrl.pathname}${nextUrl.search}`; | |
| } | |
| } | |
| return null; | |
| } | |
| async function fetchAllIssueComments(owner, repo) { | |
| const comments = []; | |
| let nextPath = `/repos/${owner}/${repo}/issues/${PR_NUMBER}/comments?per_page=100`; | |
| while (nextPath) { | |
| const pageComments = await new Promise((resolve, reject) => { | |
| const options = { | |
| hostname: "api.github.com", | |
| path: nextPath, | |
| method: "GET", | |
| headers: { | |
| Authorization: `Bearer ${GITHUB_TOKEN}`, | |
| Accept: "application/vnd.github.v3+json", | |
| "User-Agent": "keploy-design-review-agent", | |
| }, | |
| }; | |
| https | |
| .get(options, (res) => { | |
| let data = ""; | |
| res.on("data", (c) => (data += c)); | |
| res.on("end", () => { | |
| if (res.statusCode < 200 || res.statusCode >= 300) { | |
| reject( | |
| new Error( | |
| `GitHub API error ${res.statusCode} while listing PR comments. Please verify the repository, PR number, and token permissions, then retry. Response: ${data}` | |
| ) | |
| ); | |
| return; | |
| } | |
| let parsedComments; | |
| try { | |
| parsedComments = data.length > 0 ? JSON.parse(data) : []; | |
| } catch (error) { | |
| reject( | |
| new Error( | |
| `GitHub API returned invalid JSON while listing PR comments. Please retry the workflow, and if the issue persists inspect the API response body. Response: ${data}` | |
| ) | |
| ); | |
| return; | |
| } | |
| nextPath = parseNextLink(res.headers.link); | |
| resolve(Array.isArray(parsedComments) ? parsedComments : []); | |
| }); | |
| }) | |
| .on("error", reject); | |
| }); | |
| comments.push(...pageComments); | |
| } | |
| return comments; | |
| } | |
| /** | |
| * Find and delete any previous design review comment on the PR | |
| * so we don't accumulate stale comments. | |
| */ | |
| async function deletePreviousReviewComment(owner, repo) { | |
| const comments = await fetchAllIssueComments(owner, repo); |
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deletion filter matches any Bot comment containing “Keploy Design Review”, which could delete comments from other bots (or future automation) that happen to include that phrase. Consider narrowing this to the specific bot identity (e.g., c.user.login === 'github-actions[bot]' and/or a unique HTML comment marker in the body) before deleting.
Uh oh!
There was an error while loading. Please reload this page.