Skip to content

Add initial PoC for MCP Apps for select tools under Insiders #1

Add initial PoC for MCP Apps for select tools under Insiders

Add initial PoC for MCP Apps for select tools under Insiders #1

Workflow file for this run

name: UI Components Diff
on:
pull_request:
paths:
- 'ui/**'
- 'pkg/github/ui_embed.go'
- 'pkg/github/ui_resources.go'
- 'pkg/github/issues_ui.go'
permissions:
contents: read
pull-requests: write
jobs:
ui-diff:
runs-on: ubuntu-latest
steps:
- name: Check out PR code
uses: actions/checkout@v6
with:
path: pr
- name: Check out base branch
uses: actions/checkout@v6
with:
ref: ${{ github.base_ref }}
path: base
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Analyze React components
id: diff
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Extract component info from a TSX file
function analyzeComponent(content, filePath) {
const info = {
name: path.basename(filePath, path.extname(filePath)),
exports: [],
props: [],
hooks: [],
imports: { primer: [], react: [], local: [] },
stateVars: [],
effects: 0,
};
// Extract exported components (function ComponentName or export function)
const exportMatches = content.matchAll(/export\s+(?:default\s+)?function\s+(\w+)|function\s+(\w+)\s*\(/g);
for (const m of exportMatches) {
const name = m[1] || m[2];
if (name && /^[A-Z]/.test(name)) info.exports.push(name);
}
// Extract interface props
const propsMatch = content.matchAll(/interface\s+(\w*Props)\s*\{([^}]+)\}/g);
for (const m of propsMatch) {
const propsContent = m[2];
const propNames = propsContent.matchAll(/(\w+)\s*[?:]?\s*:/g);
for (const p of propNames) {
if (p[1] && !info.props.includes(p[1])) info.props.push(p[1]);
}
}
// Extract hooks usage
const hookMatches = content.matchAll(/\b(use[A-Z]\w+)\s*\(/g);
const seenHooks = new Set();
for (const m of hookMatches) {
if (!seenHooks.has(m[1])) {
seenHooks.add(m[1]);
info.hooks.push(m[1]);
}
}
// Extract useState variables
const stateMatches = content.matchAll(/\[\s*(\w+)\s*,\s*set\w+\s*\]\s*=\s*useState/g);
for (const m of stateMatches) {
info.stateVars.push(m[1]);
}
// Count useEffect calls
info.effects = (content.match(/useEffect\s*\(/g) || []).length;
// Extract Primer imports
const primerMatch = content.match(/@primer\/react['"]\s*;?\s*$/m) ||
content.match(/from\s+['"]@primer\/react['"]/);
if (primerMatch) {
const importLine = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]@primer\/react['"]/);
if (importLine) {
info.imports.primer = importLine[1].split(',').map(s => s.trim()).filter(Boolean);
}
}
// Extract React imports
const reactMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]react['"]/);
if (reactMatch) {
info.imports.react = reactMatch[1].split(',').map(s => s.trim()).filter(Boolean);
}
// Extract local component imports
const localMatches = content.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]\.\.?\//g);
for (const m of localMatches) {
const imports = m[1].split(',').map(s => s.trim()).filter(Boolean);
info.imports.local.push(...imports);
}
return info;
}
// Recursively find all TSX files
function findTsxFiles(dir) {
const files = [];
if (!fs.existsSync(dir)) return files;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules') {
files.push(...findTsxFiles(fullPath));
} else if (entry.name.endsWith('.tsx')) {
files.push(fullPath);
}
}
return files;
}
// Get relative path for display
function relPath(fullPath, base) {
return fullPath.replace(base + '/', '');
}
const baseDir = 'base/ui/src';
const prDir = 'pr/ui/src';
const baseFiles = findTsxFiles(baseDir);
const prFiles = findTsxFiles(prDir);
const baseComponents = new Map();
const prComponents = new Map();
for (const f of baseFiles) {
const rel = relPath(f, baseDir);
const content = fs.readFileSync(f, 'utf8');
baseComponents.set(rel, analyzeComponent(content, f));
}
for (const f of prFiles) {
const rel = relPath(f, prDir);
const content = fs.readFileSync(f, 'utf8');
prComponents.set(rel, analyzeComponent(content, f));
}
// Generate report
let report = '# UI Components Diff Report\n\n';
let hasChanges = false;
// New components
const newFiles = [...prComponents.keys()].filter(k => !baseComponents.has(k));
if (newFiles.length > 0) {
hasChanges = true;
report += '## 🆕 New Components\n\n';
for (const f of newFiles) {
const info = prComponents.get(f);
report += `### \`${f}\`\n`;
if (info.exports.length) report += `- **Exports:** ${info.exports.join(', ')}\n`;
if (info.props.length) report += `- **Props:** ${info.props.join(', ')}\n`;
if (info.hooks.length) report += `- **Hooks:** ${info.hooks.join(', ')}\n`;
if (info.stateVars.length) report += `- **State:** ${info.stateVars.join(', ')}\n`;
if (info.imports.primer.length) report += `- **Primer components:** ${info.imports.primer.join(', ')}\n`;
report += '\n';
}
}
// Removed components
const removedFiles = [...baseComponents.keys()].filter(k => !prComponents.has(k));
if (removedFiles.length > 0) {
hasChanges = true;
report += '## 🗑️ Removed Components\n\n';
for (const f of removedFiles) {
report += `- \`${f}\`\n`;
}
report += '\n';
}
// Modified components
const modifiedFiles = [...prComponents.keys()].filter(k => baseComponents.has(k));
const changes = [];
for (const f of modifiedFiles) {
const base = baseComponents.get(f);
const pr = prComponents.get(f);
const diff = { file: f, changes: [] };
// Compare exports
const newExports = pr.exports.filter(e => !base.exports.includes(e));
const removedExports = base.exports.filter(e => !pr.exports.includes(e));
if (newExports.length) diff.changes.push(`Added exports: ${newExports.join(', ')}`);
if (removedExports.length) diff.changes.push(`Removed exports: ${removedExports.join(', ')}`);
// Compare props
const newProps = pr.props.filter(p => !base.props.includes(p));
const removedProps = base.props.filter(p => !pr.props.includes(p));
if (newProps.length) diff.changes.push(`Added props: ${newProps.join(', ')}`);
if (removedProps.length) diff.changes.push(`Removed props: ${removedProps.join(', ')}`);
// Compare hooks
const newHooks = pr.hooks.filter(h => !base.hooks.includes(h));
const removedHooks = base.hooks.filter(h => !pr.hooks.includes(h));
if (newHooks.length) diff.changes.push(`Added hooks: ${newHooks.join(', ')}`);
if (removedHooks.length) diff.changes.push(`Removed hooks: ${removedHooks.join(', ')}`);
// Compare state
const newState = pr.stateVars.filter(s => !base.stateVars.includes(s));
const removedState = base.stateVars.filter(s => !pr.stateVars.includes(s));
if (newState.length) diff.changes.push(`Added state: ${newState.join(', ')}`);
if (removedState.length) diff.changes.push(`Removed state: ${removedState.join(', ')}`);
// Compare effects
if (pr.effects !== base.effects) {
diff.changes.push(`Effects: ${base.effects} → ${pr.effects}`);
}
// Compare Primer imports
const newPrimer = pr.imports.primer.filter(p => !base.imports.primer.includes(p));
const removedPrimer = base.imports.primer.filter(p => !pr.imports.primer.includes(p));
if (newPrimer.length) diff.changes.push(`Added Primer: ${newPrimer.join(', ')}`);
if (removedPrimer.length) diff.changes.push(`Removed Primer: ${removedPrimer.join(', ')}`);
if (diff.changes.length > 0) {
changes.push(diff);
}
}
if (changes.length > 0) {
hasChanges = true;
report += '## ✏️ Modified Components\n\n';
for (const diff of changes) {
report += `### \`${diff.file}\`\n`;
for (const change of diff.changes) {
report += `- ${change}\n`;
}
report += '\n';
}
}
if (!hasChanges) {
report += '✅ No significant React component changes detected.\n';
}
// Write report
fs.writeFileSync('diff-report.md', report);
// Output to step summary
const summary = process.env.GITHUB_STEP_SUMMARY;
fs.appendFileSync(summary, report);
core.setOutput('has_changes', hasChanges ? 'true' : 'false');
- name: Check if already commented
if: steps.diff.outputs.has_changes == 'true'
id: check_comment
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const existingComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('# UI Components Diff Report')
);
if (existingComment) {
core.setOutput('comment_id', existingComment.id);
}
- name: Post or update PR comment
if: steps.diff.outputs.has_changes == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('diff-report.md', 'utf8');
const commentId = '${{ steps.check_comment.outputs.comment_id }}';
const body = `${report}
---
<sub>🤖 This comment is automatically updated when UI component files change.</sub>`;
if (commentId) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: parseInt(commentId),
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}