Add initial PoC for MCP Apps for select tools under Insiders #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); | |
| } |