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');
}