/**
* @fileoverview Handlers for advanced GitHub features.
* This file contains real implementations for advanced GitHub functionality.
*/
const { validateRequired, getOwnerRepo } = require("../utils/shared-utils.cjs");
/**
* Handler for code quality checks using GitHub's built-in code scanning.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Code scanning results or setup instructions.
*/
async function handleCodeQualityChecks(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { tool_name = "codeql" } = args;
// Check for existing code scanning alerts
const alerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/code-scanning/alerts`
);
// Get code scanning analyses
let analyses = [];
try {
analyses = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/code-scanning/analyses`
);
} catch (error) {
// Code scanning might not be enabled
if (error.message.includes('404')) {
return {
success: true,
message: "Code scanning is not enabled for this repository.",
data: {
alerts: [],
analyses: [],
recommendations: [
"Enable GitHub Advanced Security to use code scanning",
"Set up CodeQL analysis workflow in .github/workflows/",
"Configure third-party security tools like Snyk or SonarCloud"
],
setup_url: `https://github.com/${owner}/${repo}/security/code-scanning`
}
};
}
throw error;
}
// Analyze the results
const highSeverityAlerts = alerts.filter(alert =>
alert.rule?.severity === 'error' || alert.rule?.security_severity_level === 'high'
);
const mediumSeverityAlerts = alerts.filter(alert =>
alert.rule?.severity === 'warning' || alert.rule?.security_severity_level === 'medium'
);
return {
success: true,
message: `Code quality analysis completed for ${owner}/${repo}`,
data: {
repository: `${owner}/${repo}`,
total_alerts: alerts.length,
high_severity: highSeverityAlerts.length,
medium_severity: mediumSeverityAlerts.length,
low_severity: alerts.length - highSeverityAlerts.length - mediumSeverityAlerts.length,
recent_analyses: analyses.slice(0, 5).map(analysis => ({
id: analysis.id,
tool: analysis.tool?.name || 'Unknown',
created_at: analysis.created_at,
commit_sha: analysis.commit_sha?.substring(0, 7),
results_count: analysis.results_count || 0
})),
alerts_url: `https://github.com/${owner}/${repo}/security/code-scanning`,
recommendations: alerts.length > 0 ?
["Review and fix high-severity alerts", "Set up automated security scanning"] :
["Code scanning is active with no current alerts", "Consider adding more security tools"]
}
};
}
/**
* Handler for custom dashboard data aggregation from GitHub API.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Custom dashboard data with repository metrics.
*/
async function handleCustomDashboards(args, defaultRepo, apiService) {
const { action = "view", dashboard_id = "default" } = args;
if (action === "view") {
// Determine scope - repo-specific or user-wide
let dashboardData = {};
if (args.owner && args.repo) {
// Repository-specific dashboard
const { owner, repo } = getOwnerRepo(args, defaultRepo);
// Fetch repository metrics
const [repoInfo, issues, prs, commits, contributors, languages] = await Promise.all([
apiService.makeGitHubRequest(`/repos/${owner}/${repo}`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/issues?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/pulls?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/commits?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/contributors?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/languages`)
]);
// Process metrics
const openIssues = issues.filter(issue => issue.state === 'open' && !issue.pull_request);
const closedIssues = issues.filter(issue => issue.state === 'closed' && !issue.pull_request);
const openPRs = prs.filter(pr => pr.state === 'open');
const mergedPRs = prs.filter(pr => pr.merged_at);
// Recent activity (last 30 days)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentCommits = commits.filter(commit =>
new Date(commit.commit.author.date) > thirtyDaysAgo
);
dashboardData = {
repository: `${owner}/${repo}`,
overview: {
stars: repoInfo.stargazers_count,
forks: repoInfo.forks_count,
watchers: repoInfo.subscribers_count,
size_kb: repoInfo.size,
default_branch: repoInfo.default_branch,
visibility: repoInfo.private ? 'private' : 'public'
},
issues: {
total: issues.length,
open: openIssues.length,
closed: closedIssues.length,
open_ratio: issues.length > 0 ? (openIssues.length / issues.length * 100).toFixed(1) : 0
},
pull_requests: {
total: prs.length,
open: openPRs.length,
merged: mergedPRs.length,
merge_ratio: prs.length > 0 ? (mergedPRs.length / prs.length * 100).toFixed(1) : 0
},
activity: {
total_commits: commits.length,
commits_last_30_days: recentCommits.length,
contributors: contributors.length,
top_contributor: contributors[0]?.login || 'N/A',
last_update: repoInfo.updated_at
},
languages: Object.keys(languages).map(lang => ({
name: lang,
bytes: languages[lang],
percentage: ((languages[lang] / Object.values(languages).reduce((a, b) => a + b, 0)) * 100).toFixed(1)
})).slice(0, 5),
health_score: calculateRepoHealthScore(repoInfo, openIssues, mergedPRs, recentCommits)
};
} else {
// User-wide dashboard
const user = await apiService.makeGitHubRequest('/user');
const repos = await apiService.makeGitHubRequest('/user/repos?per_page=100&sort=updated');
const totalStars = repos.reduce((sum, repo) => sum + repo.stargazers_count, 0);
const totalForks = repos.reduce((sum, repo) => sum + repo.forks_count, 0);
const publicRepos = repos.filter(repo => !repo.private);
const privateRepos = repos.filter(repo => repo.private);
dashboardData = {
user: user.login,
profile: {
name: user.name,
public_repos: user.public_repos,
followers: user.followers,
following: user.following,
created_at: user.created_at
},
repositories: {
total: repos.length,
public: publicRepos.length,
private: privateRepos.length,
total_stars: totalStars,
total_forks: totalForks,
most_starred: repos.sort((a, b) => b.stargazers_count - a.stargazers_count)[0]?.name || 'N/A'
},
recent_activity: repos.slice(0, 10).map(repo => ({
name: repo.name,
updated: repo.updated_at,
stars: repo.stargazers_count,
language: repo.language
}))
};
}
return {
success: true,
message: `Dashboard ${dashboard_id} generated successfully`,
data: {
dashboard_id,
generated_at: new Date().toISOString(),
...dashboardData
}
};
}
return {
success: false,
message: `Unsupported dashboard action: ${action}`
};
}
/**
* Calculate repository health score based on various metrics
*/
function calculateRepoHealthScore(repo, openIssues, mergedPRs, recentCommits) {
let score = 50; // Base score
// Documentation
if (repo.description) score += 5;
if (repo.homepage) score += 5;
// Activity
const daysSinceUpdate = (Date.now() - new Date(repo.updated_at)) / (1000 * 60 * 60 * 24);
if (daysSinceUpdate < 30) score += 15;
else if (daysSinceUpdate < 90) score += 10;
else if (daysSinceUpdate < 365) score += 5;
// Issue management
if (openIssues.length < 10) score += 10;
else if (openIssues.length < 50) score += 5;
// Recent activity
if (recentCommits.length > 10) score += 10;
else if (recentCommits.length > 5) score += 5;
// Community
if (repo.stargazers_count > 100) score += 5;
if (repo.forks_count > 10) score += 5;
return Math.min(100, Math.max(0, score));
}
/**
* Handler for automated repository reporting.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Generated report data.
*/
async function handleAutomatedReporting(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { report_type = "activity", output_format = "json" } = args;
let reportData = {};
const reportId = `${owner}-${repo}-${report_type}-${Date.now()}`;
switch (report_type) {
case "activity":
// Generate activity report
const [commits, issues, prs, contributors] = await Promise.all([
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/commits?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/issues?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/pulls?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/contributors?per_page=100`)
]);
// Last 30 days activity
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentCommits = commits.filter(commit =>
new Date(commit.commit.author.date) > thirtyDaysAgo
);
const recentIssues = issues.filter(issue =>
new Date(issue.created_at) > thirtyDaysAgo
);
const recentPRs = prs.filter(pr =>
new Date(pr.created_at) > thirtyDaysAgo
);
reportData = {
repository: `${owner}/${repo}`,
period: "Last 30 days",
commits: {
total: commits.length,
recent: recentCommits.length,
top_authors: getTopCommitAuthors(recentCommits).slice(0, 5)
},
issues: {
total: issues.length,
recent: recentIssues.length,
open: issues.filter(i => i.state === 'open').length,
closed: issues.filter(i => i.state === 'closed').length
},
pull_requests: {
total: prs.length,
recent: recentPRs.length,
merged: prs.filter(pr => pr.merged_at).length,
open: prs.filter(pr => pr.state === 'open').length
},
contributors: {
total: contributors.length,
top_contributors: contributors.slice(0, 10).map(c => ({
login: c.login,
contributions: c.contributions
}))
}
};
break;
case "security":
// Generate security report
let vulnerabilityAlerts = [];
let secretScanningAlerts = [];
try {
vulnerabilityAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/vulnerability-alerts`
);
} catch (error) {
// Vulnerability alerts might not be available
}
try {
secretScanningAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/secret-scanning/alerts`
);
} catch (error) {
// Secret scanning might not be enabled
}
reportData = {
repository: `${owner}/${repo}`,
vulnerability_alerts: {
total: vulnerabilityAlerts.length,
alerts: vulnerabilityAlerts.slice(0, 10)
},
secret_scanning: {
total: secretScanningAlerts.length,
alerts: secretScanningAlerts.slice(0, 10)
},
security_recommendations: [
"Enable Dependabot alerts",
"Enable secret scanning",
"Use branch protection rules",
"Require status checks",
"Review and update dependencies regularly"
]
};
break;
case "issues":
// Generate issues report
const allIssues = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/issues?state=all&per_page=100`
);
const openIssues = allIssues.filter(issue => issue.state === 'open' && !issue.pull_request);
const closedIssues = allIssues.filter(issue => issue.state === 'closed' && !issue.pull_request);
// Issue aging
const oldIssues = openIssues.filter(issue =>
(Date.now() - new Date(issue.created_at)) > (90 * 24 * 60 * 60 * 1000)
);
reportData = {
repository: `${owner}/${repo}`,
summary: {
total_issues: openIssues.length + closedIssues.length,
open: openIssues.length,
closed: closedIssues.length,
stale_issues: oldIssues.length
},
labels: getIssueLabelStats(allIssues),
aging: {
new_7_days: openIssues.filter(i =>
(Date.now() - new Date(i.created_at)) < (7 * 24 * 60 * 60 * 1000)
).length,
older_30_days: openIssues.filter(i =>
(Date.now() - new Date(i.created_at)) > (30 * 24 * 60 * 60 * 1000)
).length,
older_90_days: oldIssues.length
}
};
break;
default:
throw new Error(`Unsupported report type: ${report_type}`);
}
return {
success: true,
message: `${report_type} report generated for ${owner}/${repo}`,
data: {
report_id: reportId,
report_type,
generated_at: new Date().toISOString(),
output_format,
...reportData
}
};
}
/**
* Helper function to get top commit authors
*/
function getTopCommitAuthors(commits) {
const authors = {};
commits.forEach(commit => {
const author = commit.commit.author.name;
authors[author] = (authors[author] || 0) + 1;
});
return Object.entries(authors)
.sort(([,a], [,b]) => b - a)
.map(([name, count]) => ({ name, commits: count }));
}
/**
* Helper function to get issue label statistics
*/
function getIssueLabelStats(issues) {
const labels = {};
issues.forEach(issue => {
issue.labels.forEach(label => {
labels[label.name] = (labels[label.name] || 0) + 1;
});
});
return Object.entries(labels)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([name, count]) => ({ name, count }));
}
/**
* Handler for GitHub notification management.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Notification management results.
*/
async function handleNotificationManagement(args, defaultRepo, apiService) {
const { action = "list", thread_id, all = false, participating = false } = args;
switch (action) {
case "list":
const params = new URLSearchParams();
if (all) params.append('all', 'true');
if (participating) params.append('participating', 'true');
const notifications = await apiService.makeGitHubRequest(
`/notifications?${params.toString()}`
);
return {
success: true,
message: `Found ${notifications.length} notifications`,
data: {
total: notifications.length,
unread: notifications.filter(n => n.unread).length,
notifications: notifications.slice(0, 20).map(notification => ({
id: notification.id,
title: notification.subject.title,
type: notification.subject.type,
repository: notification.repository.full_name,
unread: notification.unread,
updated_at: notification.updated_at,
url: notification.subject.url
}))
}
};
case "mark_read":
await apiService.makeGitHubRequest('/notifications', {
method: 'PUT'
});
return {
success: true,
message: "All notifications marked as read"
};
case "get_thread":
if (!thread_id) {
throw new Error("thread_id is required for get_thread action");
}
const thread = await apiService.makeGitHubRequest(`/notifications/threads/${thread_id}`);
return {
success: true,
message: "Thread retrieved successfully",
data: {
id: thread.id,
title: thread.subject.title,
type: thread.subject.type,
repository: thread.repository.full_name,
unread: thread.unread,
updated_at: thread.updated_at,
subscription: thread.subscription
}
};
case "mark_thread_read":
if (!thread_id) {
throw new Error("thread_id is required for mark_thread_read action");
}
await apiService.makeGitHubRequest(`/notifications/threads/${thread_id}`, {
method: 'PATCH'
});
return {
success: true,
message: `Thread ${thread_id} marked as read`
};
default:
throw new Error(`Unsupported notification action: ${action}`);
}
}
/**
* Handler for GitHub release management.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Release management results.
*/
async function handleReleaseManagement(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { action = "list", release_id, tag_name, name, body, draft = false, prerelease = false } = args;
switch (action) {
case "list":
const releases = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases?per_page=20`
);
return {
success: true,
message: `Found ${releases.length} releases for ${owner}/${repo}`,
data: {
repository: `${owner}/${repo}`,
total: releases.length,
releases: releases.map(release => ({
id: release.id,
tag_name: release.tag_name,
name: release.name,
draft: release.draft,
prerelease: release.prerelease,
published_at: release.published_at,
assets_count: release.assets.length,
download_count: release.assets.reduce((sum, asset) => sum + asset.download_count, 0),
url: release.html_url
}))
}
};
case "get_latest":
const latestRelease = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/latest`
);
return {
success: true,
message: `Latest release: ${latestRelease.tag_name}`,
data: {
id: latestRelease.id,
tag_name: latestRelease.tag_name,
name: latestRelease.name,
body: latestRelease.body,
draft: latestRelease.draft,
prerelease: latestRelease.prerelease,
published_at: latestRelease.published_at,
assets: latestRelease.assets.map(asset => ({
name: asset.name,
size: asset.size,
download_count: asset.download_count,
download_url: asset.browser_download_url
})),
url: latestRelease.html_url
}
};
case "get":
if (!release_id) {
throw new Error("release_id is required for get action");
}
const release = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/${release_id}`
);
return {
success: true,
message: `Release ${release.tag_name} retrieved`,
data: {
id: release.id,
tag_name: release.tag_name,
name: release.name,
body: release.body,
draft: release.draft,
prerelease: release.prerelease,
published_at: release.published_at,
assets: release.assets.map(asset => ({
name: asset.name,
size: asset.size,
download_count: asset.download_count
}))
}
};
case "create":
if (!tag_name) {
throw new Error("tag_name is required for create action");
}
const createData = {
tag_name,
name: name || tag_name,
body: body || '',
draft,
prerelease
};
if (args.target_commitish) {
createData.target_commitish = args.target_commitish;
}
const newRelease = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases`,
{
method: 'POST',
body: JSON.stringify(createData),
headers: { 'Content-Type': 'application/json' }
}
);
return {
success: true,
message: `Release ${tag_name} created successfully`,
data: {
id: newRelease.id,
tag_name: newRelease.tag_name,
name: newRelease.name,
draft: newRelease.draft,
prerelease: newRelease.prerelease,
url: newRelease.html_url
}
};
case "delete":
if (!release_id) {
throw new Error("release_id is required for delete action");
}
await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/${release_id}`,
{ method: 'DELETE' }
);
return {
success: true,
message: `Release ${release_id} deleted successfully`
};
default:
throw new Error(`Unsupported release action: ${action}`);
}
}
/**
* Handler for dependency analysis using GitHub's dependency APIs.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Dependency analysis results.
*/
async function handleDependencyAnalysis(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { report_type = "vulnerabilities" } = args;
let analysisData = {};
switch (report_type) {
case "vulnerabilities":
// Get Dependabot alerts
let vulnerabilityAlerts = [];
try {
vulnerabilityAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/dependabot/alerts`
);
} catch (error) {
if (error.message.includes('404')) {
return {
success: true,
message: "Dependabot alerts are not enabled for this repository",
data: {
repository: `${owner}/${repo}`,
dependabot_enabled: false,
vulnerabilities: [],
recommendations: [
"Enable Dependabot alerts in repository settings",
"Enable GitHub Advanced Security",
"Review dependency files (package.json, requirements.txt, etc.)"
]
}
};
}
throw error;
}
// Categorize by severity
const critical = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'critical');
const high = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'high');
const medium = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'medium');
const low = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'low');
analysisData = {
repository: `${owner}/${repo}`,
dependabot_enabled: true,
total_alerts: vulnerabilityAlerts.length,
severity_breakdown: {
critical: critical.length,
high: high.length,
medium: medium.length,
low: low.length
},
vulnerabilities: vulnerabilityAlerts.slice(0, 20).map(alert => ({
id: alert.number,
state: alert.state,
package: alert.dependency?.package?.name,
manifest_path: alert.dependency?.manifest_path,
severity: alert.security_vulnerability?.severity,
summary: alert.security_vulnerability?.summary,
created_at: alert.created_at,
fixed_at: alert.fixed_at,
url: alert.html_url
})),
recommendations: vulnerabilityAlerts.length > 0 ? [
"Review and fix critical and high severity vulnerabilities",
"Update dependencies to latest secure versions",
"Enable automated dependency updates"
] : [
"No vulnerabilities detected",
"Keep dependencies updated",
"Monitor for new vulnerabilities"
]
};
break;
case "licenses":
// For license analysis, we'd typically need to analyze dependency files
// This is a simplified version that checks known dependency files
let dependencyInfo = [];
try {
// Check for common dependency files
const packageFiles = ['package.json', 'requirements.txt', 'Gemfile', 'pom.xml', 'go.mod'];
for (const file of packageFiles) {
try {
const content = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/contents/${file}`
);
dependencyInfo.push({
file: file,
exists: true,
ecosystem: getEcosystemFromFile(file)
});
} catch (error) {
// File doesn't exist, continue
}
}
} catch (error) {
// Continue with what we have
}
analysisData = {
repository: `${owner}/${repo}`,
dependency_files: dependencyInfo,
license_analysis: {
status: "manual_review_required",
note: "Full license analysis requires specialized tools",
recommendations: [
"Use tools like FOSSA or Snyk for comprehensive license analysis",
"Review dependency licenses manually",
"Ensure license compatibility with your project"
]
}
};
break;
case "graph":
// Get dependency graph (requires GraphQL API)
analysisData = {
repository: `${owner}/${repo}`,
dependency_graph: {
status: "graphql_required",
note: "Dependency graph analysis requires GraphQL API",
alternatives: [
"Use GitHub's Insights > Dependency graph page",
"Enable Dependabot for security analysis",
"Use package manager specific tools (npm audit, pip-audit, etc.)"
]
}
};
break;
default:
throw new Error(`Unsupported dependency analysis type: ${report_type}`);
}
return {
success: true,
message: `Dependency analysis (${report_type}) completed for ${owner}/${repo}`,
data: {
analysis_type: report_type,
generated_at: new Date().toISOString(),
...analysisData
}
};
}
/**
* Helper function to determine ecosystem from dependency file
*/
function getEcosystemFromFile(filename) {
const ecosystems = {
'package.json': 'npm',
'requirements.txt': 'pip',
'Gemfile': 'ruby',
'pom.xml': 'maven',
'go.mod': 'go',
'Cargo.toml': 'cargo',
'composer.json': 'composer'
};
return ecosystems[filename] || 'unknown';
}
module.exports = {
handleCodeQualityChecks,
handleCustomDashboards,
handleAutomatedReporting,
handleNotificationManagement,
handleReleaseManagement,
handleDependencyAnalysis,
};