public static bool IsFrameworkUpToDate(string[] currentFrameworks, CakeVersion desiredCakeVersion) { // Make sure the addin currently targets at least one framework if (currentFrameworks == null || !currentFrameworks.Any()) { return(false); } // Check that all required frameworks are targeted if (desiredCakeVersion.RequiredFrameworks.Except(currentFrameworks, StringComparer.OrdinalIgnoreCase).Any()) { return(false); } // Check that no extra framework is targeted var unnecessaryFrameworks = currentFrameworks .Except(desiredCakeVersion.RequiredFrameworks, StringComparer.OrdinalIgnoreCase) .Except(desiredCakeVersion.OptionalFrameworks, StringComparer.OrdinalIgnoreCase) .ToArray(); if (unnecessaryFrameworks.Any()) { return(false); } return(true); }
public static bool IsFrameworkUpToDate(string[] currentFrameworks, CakeVersion desiredCakeVersion) { if (currentFrameworks == null || !currentFrameworks.Any()) { return(false); } else if (!currentFrameworks.Contains(desiredCakeVersion.RequiredFramework, StringComparer.InvariantCultureIgnoreCase)) { return(false); } var unnecessaryFrameworks = currentFrameworks .Except(new[] { desiredCakeVersion.RequiredFramework }) .Except(desiredCakeVersion.OptionalFrameworks) .ToArray(); if (unnecessaryFrameworks.Any()) { return(false); } return(true); }
private void GenerateExcelWorksheet(IEnumerable <AddinMetadata> addins, CakeVersion cakeVersion, AddinType type, string caption, ExcelPackage excel) { var filteredAddins = addins .Where(addin => addin.Type.IsFlagSet(type)) .ToArray(); var reportColumns = Constants.REPORT_COLUMNS .Where(column => column.Destination.HasFlag(DataDestination.Excel)) .Where(column => column.ApplicableTo.HasFlag(type)) .Select((data, index) => new { Index = index, Data = data }) .ToArray(); // Create the worksheet var worksheet = excel.Workbook.Worksheets.Add(caption); // Header row foreach (var column in reportColumns) { worksheet.Cells[1, column.Index + 1].Value = column.Data.Header; } // One row per addin var row = 1; foreach (var addin in filteredAddins.OrderBy(a => a.Name)) { row++; foreach (var column in reportColumns) { if (column.Data.ApplicableTo.HasFlag(addin.Type)) { var cell = worksheet.Cells[row, column.Index + 1]; cell.Value = column.Data.GetContent(addin); var color = column.Data.GetCellColor(addin, cakeVersion); if (color != Color.Empty) { cell.Style.Fill.PatternType = ExcelFillStyle.Solid; cell.Style.Fill.BackgroundColor.SetColor(color); } var hyperlink = column.Data.GetHyperLink(addin); if (hyperlink != null) { cell.Hyperlink = hyperlink; cell.StyleName = "HyperLink"; } } } } // Freeze the top row and first column worksheet.View.FreezePanes(2, 2); // Setup auto-filter worksheet.Cells[1, 1, 1, reportColumns.Length].AutoFilter = true; // Format the worksheet worksheet.Row(1).Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; if (filteredAddins.Any()) { foreach (var column in reportColumns) { worksheet.Cells[2, column.Index + 1, row, column.Index + 1].Style.HorizontalAlignment = column.Data.Align; } } // Resize columns worksheet.Cells[1, 1, row, reportColumns.Length].AutoFitColumns(); // Make columns a little bit wider to account for the filter "drop-down arrow" button foreach (var column in reportColumns) { worksheet.Column(column.Index + 1).Width += 2.14; } }
private string GenerateMarkdown(DiscoveryContext context, IEnumerable <AddinMetadata> addins, CakeVersion cakeVersion, AddinType type) { var filteredAddins = addins .Where(addin => string.IsNullOrEmpty(addin.AnalysisResult.Notes)) .Where(addin => addin.Type == type) .ToArray(); var reportColumns = Constants.REPORT_COLUMNS .Where(column => column.Destination.HasFlag(GetMarkdownDestinationForType(type))) .Where(column => column.ApplicableTo.HasFlag(type)) .Select((data, index) => new { Index = index, Data = data }) .ToArray(); var now = DateTime.UtcNow; var markdown = new StringBuilder(); if (cakeVersion != null) { markdown.AppendLine($"# Audit Report for Cake {cakeVersion.Version}"); } else { markdown.AppendLine("# Audit Report"); } markdown.AppendLine(); markdown.AppendLine($"This report was generated by Cake.AddinDiscoverer {context.Version} on {now.ToLongDateString()} at {now.ToLongTimeString()} GMT"); markdown.AppendLine(); if (type == AddinType.Addin) { markdown.AppendLine("- The `Cake Core Version` and `Cake Common Version` columns show the version referenced by a given addin"); markdown.AppendLine($"- The `Cake Core IsPrivate` and `Cake Common IsPrivate` columns indicate whether the references are marked as private. In other words, we are looking for references with the `PrivateAssets=All` attribute like in this example: `<PackageReference Include=\"Cake.Common\" Version=\"{cakeVersion.Version}\" PrivateAssets=\"All\" />`"); markdown.AppendLine($"- The `Framework` column shows the .NET framework(s) targeted by a given addin. Addins should target {cakeVersion.RequiredFramework} at a minimum, and they can also optionally multi-target {string.Concat(" or ", cakeVersion.OptionalFrameworks)}"); } markdown.AppendLine(); if (type == AddinType.Addin) { markdown.AppendLine("## Statistics"); markdown.AppendLine(); var addinsReferencingCakeCore = filteredAddins.Where(addin => addin.Type == AddinType.Addin & addin.AnalysisResult.CakeCoreVersion != null); markdown.AppendLine($"- Of the {addinsReferencingCakeCore.Count()} audited addins that reference Cake.Core:"); markdown.AppendLine($" - {addinsReferencingCakeCore.Count(addin => addin.AnalysisResult.CakeCoreVersion.IsUpToDate(cakeVersion.Version))} are targeting the desired version of Cake.Core"); markdown.AppendLine($" - {addinsReferencingCakeCore.Count(addin => addin.AnalysisResult.CakeCoreIsPrivate)} have marked the reference to Cake.Core as private"); markdown.AppendLine(); var addinsReferencingCakeCommon = filteredAddins.Where(addin => addin.Type == AddinType.Addin & addin.AnalysisResult.CakeCommonVersion != null); markdown.AppendLine($"- Of the {addinsReferencingCakeCommon.Count()} audited addins that reference Cake.Common:"); markdown.AppendLine($" - {addinsReferencingCakeCommon.Count(addin => addin.AnalysisResult.CakeCommonVersion.IsUpToDate(cakeVersion.Version))} are targeting the desired version of Cake.Common"); markdown.AppendLine($" - {addinsReferencingCakeCommon.Count(addin => addin.AnalysisResult.CakeCommonIsPrivate)} have marked the reference to Cake.Common as private"); markdown.AppendLine(); } // Title markdown.AppendLine("## Addins"); markdown.AppendLine(); // Header row 1 foreach (var column in reportColumns) { markdown.Append($"| {column.Data.Header} "); } markdown.AppendLine("|"); // Header row 2 foreach (var column in reportColumns) { markdown.Append("| "); if (column.Data.Align == ExcelHorizontalAlignment.Center) { markdown.Append(":"); } markdown.Append("---"); if (column.Data.Align == ExcelHorizontalAlignment.Right || column.Data.Align == ExcelHorizontalAlignment.Center) { markdown.Append(":"); } markdown.Append(" "); } markdown.AppendLine("|"); // One row per addin foreach (var addin in filteredAddins.OrderBy(addin => addin.Name)) { foreach (var column in reportColumns) { if (column.Data.ApplicableTo.HasFlag(addin.Type)) { var content = column.Data.GetContent(addin); var hyperlink = column.Data.GetHyperLink(addin); var color = column.Data.GetCellColor(addin, cakeVersion); var emoji = string.Empty; if (color == Color.LightGreen) { emoji = Constants.GREEN_EMOJI; } else if (color == Color.Red) { emoji = Constants.RED_EMOJI; } else if (color == Color.Gold) { emoji = Constants.YELLOW_EMOJI; } if (hyperlink == null) { markdown.Append($"| {content} {emoji}"); } else { markdown.Append($"| [{content}]({hyperlink.AbsoluteUri}) {emoji}"); } } else { markdown.Append($"| "); } } markdown.AppendLine("|"); } return(markdown.ToString()); }
private async Task <RecipeFile[]> GetRecipeFilesAsync(DiscoveryContext context, CakeVersion currentCakeVersion, CakeVersion nextCakeVersion, CakeVersion latestCakeVersion) { var directoryContent = await context.GithubClient.Repository.Content.GetAllContents(Constants.CAKE_CONTRIB_REPO_OWNER, Constants.CAKE_RECIPE_REPO_NAME, "Cake.Recipe/Content").ConfigureAwait(false); var cakeFiles = directoryContent.Where(c => c.Type == new StringEnum <ContentType>(ContentType.File) && c.Name.EndsWith(".cake", StringComparison.OrdinalIgnoreCase)); var recipeFiles = await cakeFiles .ForEachAsync( async cakeFile => { var contents = await context.GithubClient.Repository.Content.GetAllContents(Constants.CAKE_CONTRIB_REPO_OWNER, Constants.CAKE_RECIPE_REPO_NAME, cakeFile.Path).ConfigureAwait(false); var recipeFile = new RecipeFile() { Name = cakeFile.Name, Path = cakeFile.Path, Content = contents[0].Content }; foreach (var addinReference in recipeFile.AddinReferences) { addinReference.LatestVersionForCurrentCake = context.Addins.SingleOrDefault(addin => { return(addin.Name.Equals(addinReference.Name, StringComparison.OrdinalIgnoreCase) && !addin.IsPrerelease && (currentCakeVersion == null || (addin.AnalysisResult.CakeCoreVersion.IsUpToDate(currentCakeVersion.Version) && addin.AnalysisResult.CakeCommonVersion.IsUpToDate(currentCakeVersion.Version))) && (nextCakeVersion == null || !(addin.AnalysisResult.CakeCoreVersion.IsUpToDate(nextCakeVersion.Version) && addin.AnalysisResult.CakeCommonVersion.IsUpToDate(nextCakeVersion.Version)))); })?.NuGetPackageVersion; addinReference.LatestVersionForLatestCake = context.Addins.SingleOrDefault(addin => { return(addin.Name.Equals(addinReference.Name, StringComparison.OrdinalIgnoreCase) && !addin.IsPrerelease && (latestCakeVersion != null && (addin.AnalysisResult.CakeCoreVersion.IsUpToDate(latestCakeVersion.Version) && addin.AnalysisResult.CakeCommonVersion.IsUpToDate(latestCakeVersion.Version)))); })?.NuGetPackageVersion; } var nugetPackageMetadataClient = context.NugetRepository.GetResource <PackageMetadataResource>(); await recipeFile.ToolReferences.ForEachAsync( async toolReference => { var searchMetadata = await nugetPackageMetadataClient.GetMetadataAsync(toolReference.Name, false, false, new SourceCacheContext(), NullLogger.Instance, CancellationToken.None).ConfigureAwait(false); var mostRecentPackageMetadata = searchMetadata.OrderByDescending(p => p.Published).FirstOrDefault(); if (mostRecentPackageMetadata != null) { toolReference.LatestVersion = mostRecentPackageMetadata.Identity.Version.ToNormalizedString(); } }, Constants.MAX_NUGET_CONCURENCY) .ConfigureAwait(false); return(recipeFile); }, Constants.MAX_GITHUB_CONCURENCY) .ConfigureAwait(false); return(recipeFiles); }
private async Task <Issue> CreateIssueAsync(bool debugging, DiscoveryContext context, AddinMetadata addin, CakeVersion recommendedCakeVersion) { var issuesDescription = new StringBuilder(); if (addin.AnalysisResult.CakeCoreVersion == Constants.UNKNOWN_VERSION) { issuesDescription.AppendLine($"- [ ] We were unable to determine what version of Cake.Core your addin is referencing. Please make sure you are referencing {recommendedCakeVersion.Version}"); } else if (!addin.AnalysisResult.CakeCoreVersion.IsUpToDate(recommendedCakeVersion.Version)) { issuesDescription.AppendLine($"- [ ] You are currently referencing Cake.Core {addin.AnalysisResult.CakeCoreVersion}. Please upgrade to {recommendedCakeVersion.Version}"); } if (addin.AnalysisResult.CakeCommonVersion == Constants.UNKNOWN_VERSION) { issuesDescription.AppendLine($"- [ ] We were unable to determine what version of Cake.Common your addin is referencing. Please make sure you are referencing {recommendedCakeVersion.Version}"); } else if (!addin.AnalysisResult.CakeCommonVersion.IsUpToDate(recommendedCakeVersion.Version)) { issuesDescription.AppendLine($"- [ ] You are currently referencing Cake.Common {addin.AnalysisResult.CakeCommonVersion}. Please upgrade to {recommendedCakeVersion.Version}"); } if (!addin.AnalysisResult.CakeCoreIsPrivate) { issuesDescription.AppendLine($"- [ ] The Cake.Core reference should be private. Specifically, your addin's `.csproj` should have a line similar to this: `<PackageReference Include=\"Cake.Core\" Version=\"{recommendedCakeVersion.Version}\" PrivateAssets=\"All\" />`"); } if (!addin.AnalysisResult.CakeCommonIsPrivate) { issuesDescription.AppendLine($"- [ ] The Cake.Common reference should be private. Specifically, your addin's `.csproj` should have a line similar to this: `<PackageReference Include=\"Cake.Common\" Version=\"{recommendedCakeVersion.Version}\" PrivateAssets=\"All\" />`"); } if (!Misc.IsFrameworkUpToDate(addin.Frameworks, recommendedCakeVersion)) { issuesDescription.AppendLine($"- [ ] Your addin should target {recommendedCakeVersion.RequiredFramework} at a minimum. Optionally, your addin can also multi-target {string.Join(" or ", recommendedCakeVersion.OptionalFrameworks)}."); } switch (addin.AnalysisResult.Icon) { case IconAnalysisResult.Unspecified: case IconAnalysisResult.RawgitUrl: case IconAnalysisResult.CustomUrl: issuesDescription.AppendLine("- [ ] The nuget package for your addin should embed the cake-contrib icon. Specifically, your addin's `.csproj` should have a line like this: `<PackageIcon>path/to/icon.png</PackageIcon>`."); break; case IconAnalysisResult.EmbeddedCustom: issuesDescription.AppendLine("- [ ] The icon embedded in your nuget package doesn't appear to be the cake-contrib icon. We strongly recommend that you use the icon [available here](https://github.com/cake-contrib/graphics/blob/master/png/cake-contrib-medium.png)."); break; } if (issuesDescription.Length == 0) { return(null); } // Create a new issue var issueBody = $"We performed an automated audit of your Cake addin and found that it does not follow all the best practices.{Environment.NewLine}{Environment.NewLine}"; issueBody += $"We encourage you to make the following modifications:{Environment.NewLine}{Environment.NewLine}"; issueBody += issuesDescription.ToString(); issueBody += $"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}"; issueBody += $"Apologies if this is already being worked on, or if there are existing open issues, this issue was created based on what is currently published for this package on NuGet.{Environment.NewLine}"; issueBody += $"{Environment.NewLine}This issue was created by a tool: Cake.AddinDiscoverer version {context.Version}{Environment.NewLine}"; var newIssue = new NewIssue(Constants.ISSUE_TITLE) { Body = issueBody.ToString() }; Issue issue = null; try { if (debugging) { await File.WriteAllTextAsync(Path.Combine(context.TempFolder, $"Issue_{addin.Name}.txt"), issueBody.ToString()).ConfigureAwait(false); } else { issue = await context.GithubClient.Issue.Create(addin.RepositoryOwner, addin.RepositoryName, newIssue).ConfigureAwait(false); } } catch (ApiException e) when(e.ApiError.Message.EqualsIgnoreCase("Issues are disabled for this repo")) { // There's a NuGet package with a project URL that points to a fork which doesn't allow issues. // Therefore it's safe to ignore this error. } catch (ApiException e) when(e.StatusCode == System.Net.HttpStatusCode.NotFound) { // I know of at least one case where the URL in the NuGet metadata points to a repo that has been deleted. // Therefore it's safe to ignore this error. } #pragma warning disable CS0168 // Variable is declared but never used catch (Exception e) #pragma warning restore CS0168 // Variable is declared but never used { Debugger.Break(); throw; } finally { // This delay is important to avoid triggering GitHub's abuse protection await Misc.RandomGithubDelayAsync().ConfigureAwait(false); } return(issue); }
private async Task FixNuspec(DiscoveryContext context, AddinMetadata addin, CakeVersion cakeVersion, IList <(string CommitMessage, IEnumerable <string> FilesToDelete, IEnumerable <(EncodingType Encoding, string Path, string Content)> FilesToUpsert)> commits)