Skip to content

feat: ai for ci diff action#42

Open
yifancong wants to merge 1 commit intomainfrom
feat/json-diff-ai
Open

feat: ai for ci diff action#42
yifancong wants to merge 1 commit intomainfrom
feat/json-diff-ai

Conversation

@yifancong
Copy link
Copy Markdown
Contributor

This pull request introduces an AI-powered degradation analysis feature to the Rsdoctor GitHub Action, enabling automated bundle size regression reports using Anthropic or OpenAI models. The main changes add a new input for selecting the AI model, implement logic to generate and analyze bundle diffs with AI, and update the PR comment to include the AI's findings.

AI analysis integration:

  • Added a new ai_model input to action.yml to allow users to specify which AI model to use for degradation analysis; the provider is auto-detected from the model name.
  • Implemented src/ai-analysis.ts, which detects the provider, builds prompts, and calls Anthropic or OpenAI APIs to analyze bundle diff JSONs and return concise markdown reports.
  • Updated src/index.ts to:
    • Import and use the new AI analysis module, and extend the ProjectReport type to include AI analysis results. [1] [2]
    • Pass aiToken and aiModel through the processing pipeline and generate bundle diff JSONs for AI analysis per project, handling both direct and npx-based CLI invocation. [1] [2] [3] [4]

PR comment/report enhancements:

  • Appended an expandable "AI Degradation Analysis" section to the PR comment, displaying per-project AI-generated analysis and the model used, when available.

Copilot AI review requested due to automatic review settings March 24, 2026 09:40
@github-actions
Copy link
Copy Markdown

Rsdoctor Bundle Diff Analysis

⚠️ Note: The latest commit (d3a7ae073b) does not have baseline artifacts. Using commit 2aaf6eac24 for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.

Found 2 projects in monorepo, 0 projects with changes.

📊 Quick Summary
Project Total Size Change
rsbuild-demo 190.4 KB 0
rsbuild-demo2 190.3 KB 0

Generated by Rsdoctor GitHub Action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds AI-powered degradation analysis to the Rsdoctor GitHub Action by generating bundle-diff JSON and sending it to an LLM (Anthropic/OpenAI), then appending the results to the PR comment.

Changes:

  • Added ai_model action input and plumbed AI token/model through the processing pipeline.
  • Implemented src/ai-analysis.ts to build prompts, detect provider, and call Anthropic/OpenAI APIs.
  • Enhanced PR comment output with an expandable “AI Degradation Analysis” section per project.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/index.ts Runs JSON bundle-diff generation, calls AI analysis, and appends AI results into PR comments.
src/ai-analysis.ts New module to detect provider, build prompts, call LLM APIs, and return markdown analysis.
action.yml Adds the new ai_model input for selecting the model.
dist/index.js Compiled output updated to include AI analysis behavior.

Comment on lines +285 to +287
const shellCmd = `npx @rsdoctor/cli bundle-diff --json --baseline="${baselineJsonPath}" --current="${fullPath}"`;
console.log(`🛠️ Running rsdoctor --json via npx: ${shellCmd}`);
await execFileAsync('sh', ['-c', shellCmd], { cwd: tempOutDir });
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using sh -c with an interpolated command string built from file paths can enable shell injection if baselineJsonPath or fullPath contains shell metacharacters (these paths can be PR-controlled in a GitHub Action). Prefer calling execFileAsync without a shell by passing npx (or node) and arguments as an array, which avoids quoting/escaping issues and is safer.

Suggested change
const shellCmd = `npx @rsdoctor/cli bundle-diff --json --baseline="${baselineJsonPath}" --current="${fullPath}"`;
console.log(`🛠️ Running rsdoctor --json via npx: ${shellCmd}`);
await execFileAsync('sh', ['-c', shellCmd], { cwd: tempOutDir });
const npxArgs = [
'@rsdoctor/cli',
'bundle-diff',
'--json',
'--baseline',
baselineJsonPath,
'--current',
fullPath,
];
console.log(`🛠️ Running rsdoctor --json via npx with args: ${JSON.stringify(npxArgs)}`);
await execFileAsync('npx', npxArgs, { cwd: tempOutDir });

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +66
function buildPrompt(diffData: unknown): string {
// Truncate large diff data to avoid token limits (~50k chars)
const MAX_CHARS = 50000;
let diffStr = JSON.stringify(diffData, null, 2);
if (diffStr.length > MAX_CHARS) {
diffStr = diffStr.substring(0, MAX_CHARS) + '\n... (truncated due to size)';
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The truncation happens after JSON.stringify, which still requires serializing the entire diff into a string first. For very large diff JSONs this can significantly increase CPU/memory and may lead to OOM in CI. Consider truncating earlier (e.g., read the file as text and cap by bytes/chars before parsing/pretty-printing, or extract/summarize only the relevant top-level keys before stringifying).

Suggested change
function buildPrompt(diffData: unknown): string {
// Truncate large diff data to avoid token limits (~50k chars)
const MAX_CHARS = 50000;
let diffStr = JSON.stringify(diffData, null, 2);
if (diffStr.length > MAX_CHARS) {
diffStr = diffStr.substring(0, MAX_CHARS) + '\n... (truncated due to size)';
}
function summarizeDiffData(diffData: unknown, maxChars: number): string {
// If it's already a string, truncate directly without additional serialization
if (typeof diffData === 'string') {
return diffData.length > maxChars
? diffData.substring(0, maxChars) + '\n... (truncated due to size)'
: diffData;
}
// For arrays, only serialize a subset of items
if (Array.isArray(diffData)) {
const MAX_ITEMS = 50;
const sliced = diffData.slice(0, MAX_ITEMS);
let result = JSON.stringify(sliced, null, 2);
if (diffData.length > MAX_ITEMS) {
result += `\n... (${diffData.length - MAX_ITEMS} more items truncated)`;
}
if (result.length > maxChars) {
result = result.substring(0, maxChars) + '\n... (truncated due to size)';
}
return result;
}
// For plain objects, only include a subset of top-level keys
if (diffData && typeof diffData === 'object') {
const obj = diffData as Record<string, unknown>;
const keys = Object.keys(obj);
const MAX_KEYS = 50;
const limited: Record<string, unknown> = {};
for (const key of keys.slice(0, MAX_KEYS)) {
limited[key] = obj[key];
}
let result = JSON.stringify(limited, null, 2);
if (keys.length > MAX_KEYS) {
result += `\n... (${keys.length - MAX_KEYS} more keys truncated)`;
}
if (result.length > maxChars) {
result = result.substring(0, maxChars) + '\n... (truncated due to size)';
}
return result;
}
// Fallback: coerce to string and truncate if needed
const coerced = String(diffData);
return coerced.length > maxChars
? coerced.substring(0, maxChars) + '\n... (truncated due to size)'
: coerced;
}
function buildPrompt(diffData: unknown): string {
// Truncate large diff data to avoid token limits (~50k chars)
const MAX_CHARS = 50000;
const diffStr = summarizeDiffData(diffData, MAX_CHARS);

Copilot uses AI. Check for mistakes.
}

function detectProvider(model: string): 'anthropic' | 'openai' {
return model.toLowerCase().startsWith('claude') ? 'anthropic' : 'openai';
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider detection is too narrow: model identifiers like anthropic/claude-... or other Anthropic-prefixed forms won’t match startsWith('claude') and will be incorrectly routed to OpenAI. Recommend expanding detection (e.g., also accept anthropic/ prefixes and claude appearing after a namespace) and/or validating the model string and returning a clear error for unknown formats.

Suggested change
return model.toLowerCase().startsWith('claude') ? 'anthropic' : 'openai';
const lower = model.toLowerCase();
// Treat bare "claude-..." and namespaced "anthropic/claude-..." (or similar) as Anthropic
if (lower.startsWith('claude') || lower.includes('/claude')) {
return 'anthropic';
}
// Optionally recognize common OpenAI patterns explicitly; default remains OpenAI
return 'openai';

Copilot uses AI. Check for mistakes.
}

try {
const diffData: unknown = JSON.parse(fs.readFileSync(diffJsonPath, 'utf8'));
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

analyzeWithAI is async but uses readFileSync, which blocks the Node.js event loop. Switching to await fs.promises.readFile(...) keeps the action responsive and avoids blocking I/O (especially if multiple projects are processed).

Suggested change
const diffData: unknown = JSON.parse(fs.readFileSync(diffJsonPath, 'utf8'));
const diffData: unknown = JSON.parse(await fs.promises.readFile(diffJsonPath, 'utf8'));

Copilot uses AI. Check for mistakes.
Comment on lines +608 to +609
commentBody += report.aiAnalysis.analysis + '\n\n';
commentBody += `<sub>Analysis by ${report.aiAnalysis.model}</sub>\n\n`;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI output is injected directly into the PR comment body as GitHub-flavored Markdown. This can unintentionally trigger @mentions, issue/PR links, or other noisy formatting. Consider neutralizing mentions (e.g., replacing @ with @\\u200b), or constraining the rendering (e.g., wrapping the AI response in a blockquote or details section that discourages mention expansion) before posting.

Copilot uses AI. Check for mistakes.
ai_model:
description: 'AI model to use for degradation analysis (e.g. claude-3-5-haiku-latest, gpt-4o-mini). Provider is auto-detected from the model name prefix.'
required: false
default: 'claude-3-5-haiku-latest'
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other inputs in this file specify type: string, but ai_model does not. For consistency (and clearer metadata for tooling), add type: string to the new ai_model input.

Suggested change
default: 'claude-3-5-haiku-latest'
default: 'claude-3-5-haiku-latest'
type: string

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +340
const aiToken = process.env.AI_TOKEN || '';
const aiModel = getInput('ai_model') || 'claude-3-5-haiku-latest';
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action introduces ai_model as an input, but the token is only configurable via process.env.AI_TOKEN (not an action input) and isn’t documented in action.yml in this diff. Consider adding an ai_token input (and calling core.setSecret on it) or explicitly documenting the required env var in action.yml so users can discover/configure it reliably.

Copilot uses AI. Check for mistakes.
@gre
Copy link
Copy Markdown

gre commented Mar 26, 2026

That's a very interesting feature 👀 Do you plan to also make it ❌ on the PR? e.g. if some condition / based on some rules aren't satisfied (e.g; bundle size regression) could it make Github PR Status go fail?

@yifancong
Copy link
Copy Markdown
Contributor Author

That's a very interesting feature 👀 Do you plan to also make it ❌ on the PR? e.g. if some condition / based on some rules aren't satisfied (e.g; bundle size regression) could it make Github PR Status go fail?

@gre Before, I was afraid that it was not very stable at present, and I was afraid of misjudgment and interference, which would increase the threshold ability later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants