/// <summary> /// Wrapper for Github calls for each component of the overall health. /// </summary> /// <returns></returns> public override async Task <HealthMetrics> GetHealth() { Logger.Debug("GetHealth({0}/{1})", purl.Namespace, purl.Name); var health = new HealthMetrics(); var initialRateLimit = await Client.Miscellaneous.GetRateLimits(); var remaining = initialRateLimit.Resources.Core.Remaining; if (remaining < 500) { Logger.Warn("Your GitHub access token has only {0}/5000 tokens left.", remaining); } if (remaining == 0) { throw new Exception("No remaining API tokens."); } await GetProjectSizeHealth(health); await GetIssueHealth(health); await GetCommitHealth(health); await GetContributorHealth(health); await GetRecentActivityHealth(health); await GetReleaseHealth(health); await GetPullRequestHealth(health); return(health); }
/// <summary> /// Calculates pull request health /// </summary> /// <param name="metrics"></param> /// <returns></returns> public async Task GetPullRequestHealth(HealthMetrics metrics) { Logger.Debug($"GetPullRequestHealth('{purl.Namespace}', '{purl.Name}')"); double pullRequestHealth = 0; int pullRequestOpen = 0; int pullRequestClosed = 0; var pullRequestRequest = new PullRequestRequest() { State = ItemStateFilter.All }; var pullRequests = await Client.PullRequest.GetAllForRepository(purl.Namespace, purl.Name, pullRequestRequest, DEFAULT_API_OPTIONS); // Gather raw data foreach (var pullRequest in pullRequests) { if (pullRequest.State == ItemState.Open) { pullRequestOpen++; } else if (pullRequest.State == ItemState.Closed) { pullRequestClosed++; } } if (pullRequestOpen + pullRequestClosed > 0) { pullRequestHealth = 100.0 * pullRequestClosed / (pullRequestOpen + pullRequestClosed); } metrics.PullRequestHealth = pullRequestHealth; }
/// <summary> /// Calculates commit health. /// </summary> /// <returns></returns> public async Task GetCommitHealth(HealthMetrics metrics) { Logger.Debug($"GetCommitHealth('{purl.Namespace}', '{purl.Name}')"); double weightedCommits = 0.0; int totalCommits = 0; int totalRecentCommits = 0; var contributors = await Client.Repository.Statistics.GetContributors(purl.Namespace, purl.Name); totalCommits = contributors.Sum(c => c.Total); var recentCommitActivity = await Client.Repository.Statistics.GetCommitActivity(purl.Namespace, purl.Name); foreach (var weekActivity in recentCommitActivity.Activity) { totalRecentCommits += weekActivity.Total; var numDaysAgo = (DateTime.Now - weekActivity.WeekTimestamp).Days; weightedCommits += (6.0 * weekActivity.Total / Math.Log(numDaysAgo + 14)); } double commitHealth = 7.0 * Math.Log(totalCommits) + weightedCommits; metrics.CommitHealth = Math.Round(Clamp(commitHealth), 1); }
/** * Determine the most recent activity (any type). * * The API only returns events from the last 90 days * * Most recent activity. * Health calculated as the mean number of seconds in the past 30 activities. */ public async Task GetRecentActivityHealth(HealthMetrics metrics) { Logger.Trace("GetRecentActivityHealth({0}, {1})", purl.Namespace, purl.Name); var activityList = await Client.Activity.Events.GetAllForRepository(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); double activityHealth = 0.50 * activityList.Count; metrics.RecentActivityHealth = Clamp(activityHealth); }
/** * Release health is defined as: * # releases + # tags */ public async Task GetReleaseHealth(HealthMetrics metrics) { Logger.Trace("GetReleaseHealth({0}, {1})", purl.Namespace, purl.Name); var releases = await Client.Repository.Release.GetAll(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); double releaseHealth = releases.Count(); var tags = await Client.Repository.GetAllTags(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); releaseHealth += tags.Count(); metrics.ReleaseHealth = Clamp(releaseHealth); }
/** * Release health is defined as: * # releases + # tags */ public async Task GetReleaseHealth(HealthMetrics metrics) { Logger.Trace("GetReleaseHealth({0}, {1})", purl.Namespace, purl.Name); System.Collections.Generic.IReadOnlyList <Release>?releases = await Client.Repository.Release.GetAll(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); double releaseHealth = releases.Count; System.Collections.Generic.IReadOnlyList <RepositoryTag>?tags = await Client.Repository.GetAllTags(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); releaseHealth += tags.Count; metrics.ReleaseHealth = Clamp(releaseHealth); }
/// <summary> /// Calculate health based on project size. /// If the size is less than 100, then the health is the size of the /// project. If it's between 100 and 5000, it's 100%. If it's higher /// then it starts to approach zero. /// </summary> /// <returns></returns> public async Task GetProjectSizeHealth(HealthMetrics metrics) { var repo = await Client.Repository.Get(purl.Namespace, purl.Name); long health = 100; if (repo.Size < 100) { health = repo.Size; } else if (repo.Size > 10000) { health = 500000 / repo.Size; } metrics.ProjectSizeHealth = Clamp(health); }
/** * Calculates contributor health for a Repository. * * This is defined as 6 * (# contributors, subscribers, forks, stargazers), capped at 100 */ public async Task GetContributorHealth(HealthMetrics metrics) { Logger.Trace("GetContributorHealth({0}, {1}", purl.Namespace, purl.Name); var repository = await Client.Repository.Get(purl.Namespace, purl.Name); double contributorHealth = 6.0 * repository.StargazersCount + repository.SubscribersCount + repository.ForksCount; if (contributorHealth < 100.0) { var contributors = await Client.Repository.GetAllContributors(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); contributorHealth += 6.0 * contributors.Count; } metrics.ContributorHealth = Clamp(contributorHealth); }
/** * Calculates contributor health for a Repository. * * This is defined as 6 * (# contributors, subscribers, forks, stargazers), capped at 100 */ public async Task GetContributorHealth(HealthMetrics metrics) { Logger.Trace("GetContributorHealth({0}, {1}", purl.Namespace, purl.Name); Repository?repository = await Client.Repository.Get(purl.Namespace, purl.Name); double contributorHealth = 6.0 * repository.StargazersCount + repository.WatchersCount + repository.ForksCount; if (contributorHealth < 100.0) { System.Collections.Generic.IReadOnlyList <RepositoryContributor>?contributors = await Client.Repository.GetAllContributors(purl.Namespace, purl.Name, DEFAULT_API_OPTIONS); contributorHealth += 6.0 * contributors.Count; } metrics.ContributorHealth = Clamp(contributorHealth); }
/// <summary> /// Retrieves issue and security issue counts (subset) to minimize calls out. /// Note: Octokit Search API was found earlier to be unreliable /// </summary> /// <returns></returns> public async Task GetIssueHealth(HealthMetrics metrics) { Logger.Trace("GetIssueHealth({0}, {1})", purl.Namespace, purl.Name); var securityFlags = new string[] { "security", "insecure", "vulnerability", "cve", "valgrind", "xss", "sqli ", "vulnerable", "exploit", "fuzz", "injection", "buffer overflow", "valgrind", "sql injection", "csrf", "xsrf", "pwned", "akamai legacy ssl", "bad cipher ordering", "untrusted", "backdoor", "command injection" }; var filter = new RepositoryIssueRequest { Filter = IssueFilter.All, State = ItemStateFilter.All, Since = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(3 * 365)) }; int openIssues = 0; int closedIssues = 0; int openSecurityIssues = 0; int closedSecurityIssues = 0; var issues = await Client.Issue.GetAllForRepository(purl.Namespace, purl.Name, filter); foreach (var issue in issues) { // filter out pull requests if (issue.Url.Contains("/pull") || issue.HtmlUrl.Contains("/pull")) { continue; } //general issue status if (issue.State == ItemState.Open) { openIssues++; } else if (issue.State == ItemState.Closed) { closedIssues++; } // security status check within applicable fields var labels = string.Join(",", issue.Labels.Select(l => l.Name)); var content = issue.Title + issue.Body + labels; if (securityFlags.Any(s => content.Contains(s, StringComparison.InvariantCultureIgnoreCase))) { if (issue.State == ItemState.Open) { openSecurityIssues++; } else if (issue.State == ItemState.Closed) { closedSecurityIssues++; } } } double issueHealth = 0.0; if (openIssues + closedIssues > 0) { issueHealth = 30.0 * openIssues / (openIssues + closedIssues); } else { issueHealth = 50.0; } metrics.IssueHealth = issueHealth; double securityIssueHealth = 0.0; if (openSecurityIssues + closedSecurityIssues > 0) { securityIssueHealth = openSecurityIssues / (openSecurityIssues + closedSecurityIssues); } else { securityIssueHealth = 60.0; // Lose a little credit if project never had a security issue } metrics.SecurityIssueHealth = securityIssueHealth; return; }