-
-
Notifications
You must be signed in to change notification settings - Fork 289
feat: set the design guideline agent guidelines 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 all commits
c083273
80a453d
5a62c4a
133a76a
a92592a
1af086b
611f089
c10da19
69b51cf
2933d4e
8b79c38
caae559
0cc12f9
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,143 @@ | ||
| // 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"); | ||
|
|
||
| // Model is configurable via the CLAUDE_MODEL repo variable so it can be updated | ||
| // without a code change if the model is renamed or a newer version is preferred. | ||
| const CLAUDE_MODEL = process.env.CLAUDE_MODEL || "claude-sonnet-4-6"; | ||
|
|
||
| if (!process.env.GITHUB_TOKEN) { | ||
| console.error( | ||
| "ERROR: GITHUB_TOKEN is not set. This is automatically provided by GitHub Actions — " + | ||
| "ensure the workflow step has not overridden it and that the job has 'issues: write' permission." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (!process.env.GITHUB_REPOSITORY) { | ||
| console.error( | ||
| "ERROR: GITHUB_REPOSITORY is not set. This is automatically provided by GitHub Actions " + | ||
| "as 'owner/repo'. Ensure it is passed via the 'env:' block in the workflow step: " + | ||
| "GITHUB_REPOSITORY: ${{ github.repository }}" | ||
| ); | ||
| 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}`); | ||
| console.log(`Model: ${CLAUDE_MODEL}`); | ||
|
|
||
| // 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. | ||
| // Exit cleanly (exit 0) so the check doesn't fail and block the PR. | ||
| const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; | ||
| if (!ANTHROPIC_API_KEY) { | ||
| console.log("ANTHROPIC_API_KEY not available — skipping review."); | ||
| await postComment( | ||
| "## Keploy Design Review\n\n" + | ||
| "⚪ Design review was skipped because `ANTHROPIC_API_KEY` is not available in this workflow run.\n\n" + | ||
| "Possible causes: the secret is not configured in repo Settings → Secrets → Actions, " + | ||
| "it is restricted to a specific environment that this workflow cannot access, " + | ||
| "or the workflow trigger was changed from `pull_request_target` to `pull_request` " + | ||
| "which may not expose secrets. A maintainer can verify and rerun once resolved." | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Step 4: Trim the diff to only the hunks for relevant files | ||
| const filteredDiff = filterDiffToRelevantFiles(diff, relevantFiles); | ||
|
|
||
| // Step 5: Build the prompt | ||
| console.log("Building review prompt..."); | ||
| const { system, user } = buildPrompt(filteredDiff, relevantFiles); | ||
|
|
||
|
Comment on lines
+72
to
+106
|
||
| // Step 6: Call Claude API | ||
| console.log(`Calling Claude API (model: ${CLAUDE_MODEL})...`); | ||
| const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY }); | ||
|
|
||
| let message; | ||
| try { | ||
| message = await client.messages.create({ | ||
| model: CLAUDE_MODEL, | ||
| max_tokens: 4096, | ||
| messages: [{ role: "user", content: user }], | ||
| system, | ||
| }); | ||
| } catch (err) { | ||
| throw new Error( | ||
| `Claude API request failed (model: ${CLAUDE_MODEL}): ${err.message}` | ||
| ); | ||
| } | ||
|
|
||
| const reviewText = message.content | ||
| .filter((block) => block.type === "text") | ||
| .map((block) => block.text) | ||
| .join("\n"); | ||
|
|
||
| console.log("Review generated. Posting comment..."); | ||
| console.log("--- Review Preview ---"); | ||
| console.log(reviewText.slice(0, 500) + (reviewText.length > 500 ? "..." : "")); | ||
| console.log("---------------------"); | ||
|
|
||
| // Step 7: Post the comment | ||
| await postComment(reviewText); | ||
| console.log("=== Design review complete ==="); | ||
| } | ||
|
|
||
| run().catch((err) => { | ||
| console.error("Design review agent failed:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // filters a list of changed file paths to only those relevant for design review | ||
|
|
||
| const ALLOWED_EXTENSIONS = [ | ||
| ".css", | ||
| ".scss", | ||
| ".sass", | ||
| ".mdx", | ||
|
Comment on lines
+3
to
+7
|
||
| ".md", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".js", | ||
| ".ts", | ||
| ".svg", // SVGs are text diffs and design-relevant (e.g. static/keploy-logo.svg) | ||
| ]; | ||
|
|
||
| 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,102 @@ | ||
| // 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" || | ||
| GITHUB_EVENT_NAME === "pull_request_target" | ||
| ) { | ||
| // Both events carry PR_NUMBER and use the GitHub API diff endpoint. | ||
| // pull_request_target is used so secrets are available on fork PRs | ||
| // while still reviewing the actual PR changes via the API. | ||
| diff = await fetchPRDiff(); | ||
| } else if (GITHUB_EVENT_NAME === "push") { | ||
| diff = getCommitDiff(); | ||
| } else { | ||
| // workflow_dispatch or any other trigger | ||
| diff = getManualDiff(); | ||
| } | ||
|
Comment on lines
+77
to
+93
|
||
|
|
||
| // 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,9 @@ | ||
| { | ||
| "name": "keploy-design-review-agent", | ||
| "version": "1.0.0", | ||
| "description": "AI-powered design review agent for Keploy Docs", | ||
| "private": true, | ||
| "dependencies": { | ||
| "@anthropic-ai/sdk": "0.32.1" | ||
| } | ||
| } |
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.
These error logs don’t give a clear next step. Consider including actionable guidance (e.g., which workflow/env should set
GITHUB_TOKEN/GITHUB_REPOSITORY, and how to fix it) so failures are easier to diagnose when the action runs.