name: Test App on: merge_group: workflow_dispatch: push: branches: - develop pull_request: types: - opened - synchronize concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: Test: timeout-minutes: 20 runs-on: ubuntu-22.04 permissions: contents: read pull-requests: write packages: read steps: - name: Checkout branch uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm cache-dependency-path: package-lock.json registry-url: 'https://npm.pkg.github.com' scope: '@kong' - name: Install packages run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Lint run: npm run lint - name: Type checks run: npm run type-check - name: Unit Tests run: npm test - name: Check Circular References uses: actions/github-script@v7 env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: github.event_name == 'pull_request' && always() with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Function to analyze circular references async function analyzeCircularReferences() { const madge = require('madge'); const res = await madge(process.cwd(), { fileExtensions: ['ts', 'tsx'], }); return res.circular(); } // Function to generate markdown report function generateMarkdownReport(prCircularRefs, baseCircularRefs, prCount, baseCount, baseBranch) { const timestamp = new Date().toISOString(); const diff = prCount - baseCount; const diffPercent = baseCount > 0 ? ((diff / baseCount) * 100).toFixed(2) : '0.00'; let status = '✅ PASSED'; let statusEmoji = '✅'; if (diff > 0) { status = '⚠️ WARNING'; statusEmoji = '⚠️'; } else if (diff === 0) { status = '✅ NO CHANGE'; statusEmoji = '✅'; } else { status = '✨ IMPROVED'; statusEmoji = '✨'; } // Find new and removed circular references const prCycleStrings = new Set(prCircularRefs.map(cycle => cycle.join(' -> '))); const baseCycleStrings = new Set(baseCircularRefs.map(cycle => cycle.join(' -> '))); const newCycles = Array.from(prCycleStrings).filter(cycle => !baseCycleStrings.has(cycle)).sort(); const removedCycles = Array.from(baseCycleStrings).filter(cycle => !prCycleStrings.has(cycle)).sort(); return `# ${statusEmoji} Circular References Report **Generated at:** ${timestamp} **Status:** ${status} ## Summary | Metric | Base (\`${baseBranch}\`) | PR | Change | |--------|----------------|-----|---------| | Total Circular References | ${baseCount} | ${prCount} | ${diff > 0 ? '+' : ''}${diff} (${diffPercent > 0 ? '+' : ''}${diffPercent}%) | ${newCycles.length > 0 ? ` ## ⚠️ New Circular References Added (${newCycles.length})
Click to expand/collapse \`\`\` ${newCycles.join('\n')} \`\`\`
` : ''} ${removedCycles.length > 0 ? ` ## ✨ Circular References Removed (${removedCycles.length})
Click to expand/collapse \`\`\` ${removedCycles.join('\n')} \`\`\`
` : ''}
Click to view all circular references in PR (${prCount}) \`\`\` ${prCircularRefs.length > 0 ? prCircularRefs.sort().map(cycle => cycle.join(' -> ')).join('\n') : 'No circular references found'} \`\`\`
Click to view all circular references in base branch (${baseCount}) \`\`\` ${baseCircularRefs.length > 0 ? baseCircularRefs.sort().map(cycle => cycle.join(' -> ')).join('\n') : 'No circular references found'} \`\`\`
## Analysis ${ diff > 0 ? `⚠️ **Warning:** This PR introduces ${diff} new circular ${diff === 1 ? 'reference' : 'references'}. Consider refactoring to avoid adding circular dependencies.` : diff < 0 ? `✨ **Great Job!** This PR removes ${Math.abs(diff)} circular ${Math.abs(diff) === 1 ? 'reference' : 'references'}. Keep up the good work!` : `✅ **No Change:** This PR does not introduce or remove any circular references.` } --- *This report was generated automatically by comparing against the \`${baseBranch}\` branch.* `; } try { // Get base branch (the target branch of the PR) const baseBranch = context.payload.pull_request.base.ref; console.log(`Base branch (PR target): ${baseBranch}`); // Save current state const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); const hasUncommittedChanges = execSync('git status --porcelain').toString().trim().length > 0; console.log(`Current branch: ${currentBranch}`); // Stash any uncommitted changes if (hasUncommittedChanges) { console.log('Stashing uncommitted changes...'); execSync('git stash push -u -m "Temporary stash for circular reference check"'); } // Change to packages directory for analysis process.chdir('packages'); // Analyze current PR console.log('Analyzing circular references in PR...'); const prCircularRefs = await analyzeCircularReferences(); const prCount = prCircularRefs.length; console.log(`PR: Found ${prCount} circular references`); // Switch to base branch console.log(`Switching to ${baseBranch} branch...`); process.chdir('..'); execSync(`git fetch origin ${baseBranch}`); execSync(`git checkout origin/${baseBranch}`); // Reinstall dependencies for base branch (in case package.json changed) console.log('Installing dependencies for base branch...'); execSync('npm ci --quiet', { stdio: 'ignore' }); // Analyze base branch console.log(`Analyzing circular references in ${baseBranch}...`); process.chdir('packages'); const baseCircularRefs = await analyzeCircularReferences(); const baseCount = baseCircularRefs.length; console.log(`Base: Found ${baseCount} circular references`); // Switch back to PR branch console.log('Switching back to PR branch...'); process.chdir('..'); execSync(`git checkout ${currentBranch}`); // Restore stashed changes if (hasUncommittedChanges) { console.log('Restoring uncommitted changes...'); execSync('git stash pop'); } // Reinstall dependencies for PR branch console.log('Restoring dependencies for PR branch...'); execSync('npm ci --quiet', { stdio: 'ignore' }); // Generate report const diff = prCount - baseCount; const markdownContent = generateMarkdownReport(prCircularRefs, baseCircularRefs, prCount, baseCount, baseBranch); // Post PR comment if (context.eventName === 'pull_request') { try { const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const botComment = comments.find(comment => comment.user?.type === 'Bot' && comment.body?.includes('# ') && comment.body?.includes('Circular References Report') ); if (botComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: markdownContent }); console.log('Updated existing PR comment'); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: markdownContent }); console.log('Created new PR comment'); } } catch (error) { console.error('Error posting PR comment:', error); } } // Log results but don't fail the build if (diff > 0) { console.log(`⚠️ Warning: This PR introduces ${diff} new circular ${diff === 1 ? 'reference' : 'references'}. Base: ${baseCount}, PR: ${prCount}`); } else if (diff < 0) { console.log(`✨ Great! This PR removes ${Math.abs(diff)} circular ${Math.abs(diff) === 1 ? 'reference' : 'references'}!`); } else { console.log(`✅ No change in circular references (${prCount})`); } } catch (error) { console.error('Error analyzing circular references:', error); core.setFailed('Failed to analyze circular references'); }