Skip to content

Commit

Permalink
[IDP-1772] Display Spectral report in ADO CI build extension
Browse files Browse the repository at this point in the history
Don't use string length

Fix typo

fix typo

Generate spectral reports in stylish format instead of html

Use txt format

use pretty

Try html and using script

Use pretty and stylish

Remove references to html

Update readme
  • Loading branch information
heqianwang committed Aug 13, 2024
1 parent b6a0398 commit 8b55e6f
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 31 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ For the TLDR version:

- The entry point is `ValidateOpenApiTask.ExecuteAsync()` and will be executed after the referencing project is built. This is defined in `./src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets` as a `UsingTask.TaskName`
- The default value are defined in the property group on the target `ValidateOpenApi` in this file `./src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets`
- Users can select whether to validate the API with frontend or backend ruleset depending how they configure the `OpenApiServiceProfile` MSBuild property ([`backend` (default)](https://github.com/gsoft-inc/wl-api-guidelines/blob/main/.spectral.backend.yaml) or [`frontend`](https://github.com/gsoft-inc/wl-api-guidelines/blob/main/.spectral.frontend.yaml)).
- Users can select whether to validate the API with frontend or backend ruleset depending on how they configure the `OpenApiServiceProfile` MSBuild property ([`backend` (default)](https://github.com/gsoft-inc/wl-api-guidelines/blob/main/.spectral.backend.yaml) or [`frontend`](https://github.com/gsoft-inc/wl-api-guidelines/blob/main/.spectral.frontend.yaml)).
- Users can configure their CI enviroment for reports depending on how they configure the `OpenApiCiReportEnvironment` MSBuild property (`ado` (default)).

## How to test locally

Expand Down
13 changes: 13 additions & 0 deletions src/Workleap.OpenApi.MSBuild/Spectral/AdoCiReportRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Workleap.OpenApi.MSBuild.Spectral;

internal sealed class AdoCiReportRenderer : ICiReportRenderer
{
public async Task AttachReportToBuildAsync(string reportPath)
{
// Attach the report to the build summary
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_NAME")))
{
Console.WriteLine("##vso[task.addattachment type=Distributedtask.Core.Summary;name=Spectral results;]{0}", reportPath);
}
}
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/Spectral/ICiReportRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild.Spectral;

internal interface ICiReportRenderer
{
public Task AttachReportToBuildAsync(string reportPath);
}
28 changes: 16 additions & 12 deletions src/Workleap.OpenApi.MSBuild/Spectral/SpectralRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ internal sealed class SpectralRunner
private readonly ILoggerWrapper _loggerWrapper;
private readonly IProcessWrapper _processWrapper;
private readonly DiffCalculator _diffCalculator;
private readonly ICiReportRenderer _ciReportRenderer;
private readonly string _spectralDirectory;
private readonly string _openApiReportsDirectoryPath;

public SpectralRunner(
ILoggerWrapper loggerWrapper,
IProcessWrapper processWrapper,
DiffCalculator diffCalculator,
DiffCalculator diffCalculator,
ICiReportRenderer ciReportRenderer,
string openApiToolsDirectoryPath,
string openApiReportsDirectoryPath)
{
Expand All @@ -25,6 +27,7 @@ public SpectralRunner(
this._spectralDirectory = Path.Combine(openApiToolsDirectoryPath, "spectral", SpectralVersion);
this._processWrapper = processWrapper;
this._diffCalculator = diffCalculator;
this._ciReportRenderer = ciReportRenderer;
}

public async Task RunSpectralAsync(IReadOnlyCollection<string> openApiDocumentPaths, string spectralExecutablePath, string spectralRulesetPath, CancellationToken cancellationToken)
Expand All @@ -50,19 +53,20 @@ public async Task RunSpectralAsync(IReadOnlyCollection<string> openApiDocumentPa
foreach (var documentPath in openApiDocumentPaths)
{
var documentName = Path.GetFileNameWithoutExtension(documentPath);
var htmlReportPath = this.GetReportPath(documentPath);
var spectralReportPath = this.GetReportPath(documentPath);

this._loggerWrapper.LogMessage("\n *** Spectral: Validating {0} against ruleset ***", MessageImportance.High, documentName);
this._loggerWrapper.LogMessage("- File path: {0}", MessageImportance.High, documentPath);
this._loggerWrapper.LogMessage("- Ruleset : {0}\n", MessageImportance.High, spectralRulesetPath);

if (File.Exists(htmlReportPath))
if (File.Exists(spectralReportPath))
{
this._loggerWrapper.LogMessage("\nDeleting existing report: {0}", messageArgs: htmlReportPath);
File.Delete(htmlReportPath);
this._loggerWrapper.LogMessage("\nDeleting existing report: {0}", messageArgs: spectralReportPath);
File.Delete(spectralReportPath);
}

await this.GenerateSpectralReport(spectralExecutePath, documentPath, spectralRulesetPath, htmlReportPath, cancellationToken);
await this.GenerateSpectralReport(spectralExecutePath, documentPath, spectralRulesetPath, spectralReportPath, cancellationToken);
await this._ciReportRenderer.AttachReportToBuildAsync(spectralReportPath);
this._loggerWrapper.LogMessage("\n ****************************************************************", MessageImportance.High);
}

Expand All @@ -87,11 +91,11 @@ private async Task<bool> ShouldRunSpectral(string spectralRulesetPath, IReadOnly
private string GetReportPath(string documentPath)
{
var documentName = Path.GetFileNameWithoutExtension(documentPath);
var outputSpectralReportName = $"spectral-{documentName}.html";
var outputSpectralReportName = $"spectral-{documentName}.txt";
return Path.Combine(this._openApiReportsDirectoryPath, outputSpectralReportName);
}

private async Task GenerateSpectralReport(string spectralExecutePath, string swaggerDocumentPath, string rulesetPath, string htmlReportPath, CancellationToken cancellationToken)
private async Task GenerateSpectralReport(string spectralExecutePath, string swaggerDocumentPath, string rulesetPath, string spectralReportPath, CancellationToken cancellationToken)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Expand All @@ -100,25 +104,25 @@ private async Task GenerateSpectralReport(string spectralExecutePath, string swa
}

this._loggerWrapper.LogMessage("Running Spectral...", MessageImportance.Normal);
var result = await this._processWrapper.RunProcessAsync(spectralExecutePath, new[] { "lint", swaggerDocumentPath, "--ruleset", rulesetPath, "--format", "html", "--format", "pretty", "--output.html", htmlReportPath, "--fail-severity=warn", "--verbose" }, cancellationToken);
var result = await this._processWrapper.RunProcessAsync(spectralExecutePath, new[] { "lint", swaggerDocumentPath, "--ruleset", rulesetPath, "--format", "stylish", "--output", spectralReportPath, "--fail-severity=warn", "--verbose" }, cancellationToken);

this._loggerWrapper.LogMessage(result.StandardOutput, MessageImportance.High);
if (!string.IsNullOrEmpty(result.StandardError))
{
this._loggerWrapper.LogWarning(result.StandardError);
}

if (!File.Exists(htmlReportPath))
if (!File.Exists(spectralReportPath))
{
throw new OpenApiTaskFailedException($"Spectral report for {swaggerDocumentPath} could not be created. Please check the CONSOLE output above for more details.");
}

if (result.ExitCode != 0)
{
this._loggerWrapper.LogWarning($"Spectral scan detected violation of ruleset. Please check the report [{htmlReportPath}] for more details.");
this._loggerWrapper.LogWarning($"Spectral scan detected violation of ruleset. Please check the report [{spectralReportPath}] for more details.");
}

this._loggerWrapper.LogMessage("Spectral report generated. {0}", messageArgs: htmlReportPath);
this._loggerWrapper.LogMessage("Spectral report generated. {0}", messageArgs: spectralReportPath);
}

private async Task AssignExecutePermission(string spectralExecutePath, CancellationToken cancellationToken)
Expand Down
64 changes: 46 additions & 18 deletions src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed class ValidateOpenApiTask : CancelableAsyncTask
private const string ContractFirst = "ContractFirst"; // For backward compatibility
private const string Backend = "backend";
private const string Frontend = "frontend";
private const string Ado = "ado";

/// <summary>
/// 2 supported modes:
Expand All @@ -21,13 +22,20 @@ public sealed class ValidateOpenApiTask : CancelableAsyncTask
public string OpenApiDevelopmentMode { get; set; } = string.Empty;

/// <summary>
/// 2 supported profiles]:
/// 2 supported profiles:
/// - backend (default): Uses the backend ruleset to validate the API spec
/// - frontend: Uses the frontend ruleset to validate the API spec
/// </summary>
[Required]
public string OpenApiServiceProfile { get; set; } = string.Empty;

/// <summary>
/// 1 supported CI environment for Spectral report export:
/// - ado (default): Exports the Spectral report in an ADO compatible format
/// </summary>
[Required]
public string OpenApiCiReportEnvironment { get; set; } = string.Empty;

/// <summary>When Development mode is ValidateContract, will validate if the specification match the code.</summary>
[Required]
public bool OpenApiCompareCodeAgainstSpecFile { get; set; } = false;
Expand Down Expand Up @@ -64,24 +72,9 @@ protected override async Task<bool> ExecuteAsync(CancellationToken cancellationT

loggerWrapper.LogMessage("\n******** Starting {0} ********\n", MessageImportance.Normal, nameof(ValidateOpenApiTask));

var reportsPath = Path.Combine(this.OpenApiToolsDirectoryPath, "reports");
var processWrapper = new ProcessWrapper(this.StartupAssemblyPath);
var swaggerManager = new SwaggerManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiWebApiAssemblyPath);
var diffCalculator = new DiffCalculator(Path.Combine(this.OpenApiToolsDirectoryPath, "spectral-state"));

var httpClientWrapper = new HttpClientWrapper();

var spectralRulesetManager = new SpectralRulesetManager(loggerWrapper, httpClientWrapper, this.OpenApiServiceProfile, this.OpenApiSpectralRulesetUrl);
var spectralInstaller = new SpectralInstaller(loggerWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper);
var spectralManager = new SpectralRunner(loggerWrapper, processWrapper, diffCalculator, this.OpenApiToolsDirectoryPath, reportsPath);
var oasdiffManager = new OasdiffManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper);
var specGeneratorManager = new SpecGeneratorManager(loggerWrapper);

var generateContractProcess = new GenerateContractProcess(loggerWrapper, spectralInstaller, spectralRulesetManager, spectralManager, swaggerManager, specGeneratorManager, oasdiffManager);
var validateContractProcess = new ValidateContractProcess(loggerWrapper, spectralInstaller, spectralRulesetManager, spectralManager, swaggerManager, oasdiffManager);

loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Normal, nameof(this.OpenApiDevelopmentMode), this.OpenApiDevelopmentMode);
loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Normal, nameof(this.OpenApiServiceProfile), this.OpenApiServiceProfile);
loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Normal, nameof(this.OpenApiCiReportEnvironment), this.OpenApiCiReportEnvironment);
loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Normal, nameof(this.OpenApiCompareCodeAgainstSpecFile), this.OpenApiCompareCodeAgainstSpecFile);
loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Low, nameof(this.OpenApiTreatWarningsAsErrors), this.OpenApiTreatWarningsAsErrors);
loggerWrapper.LogMessage("{0} = '{1}'", MessageImportance.Low, nameof(this.OpenApiWebApiAssemblyPath), this.OpenApiWebApiAssemblyPath);
Expand All @@ -93,7 +86,6 @@ protected override async Task<bool> ExecuteAsync(CancellationToken cancellationT
if (this.OpenApiSpecificationFiles.Length != this.OpenApiSwaggerDocumentNames.Length)
{
loggerWrapper.LogWarning("You must provide the same amount of OpenAPI documents file names and swagger document file names.");

return false;
}

Expand All @@ -103,6 +95,30 @@ protected override async Task<bool> ExecuteAsync(CancellationToken cancellationT
return false;
}

if (!this.OpenApiCiReportEnvironment.Equals(Ado, StringComparison.Ordinal))
{
loggerWrapper.LogWarning("Invalid value of '{0}' for {1}. Allowed value is {2}", this.OpenApiCiReportEnvironment, nameof(this.OpenApiCiReportEnvironment), Ado);
return false;
}

var reportsPath = Path.Combine(this.OpenApiToolsDirectoryPath, "reports");
var processWrapper = new ProcessWrapper(this.StartupAssemblyPath);
var swaggerManager = new SwaggerManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiWebApiAssemblyPath);
var diffCalculator = new DiffCalculator(Path.Combine(this.OpenApiToolsDirectoryPath, "spectral-state"));

var httpClientWrapper = new HttpClientWrapper();

var spectralRulesetManager = new SpectralRulesetManager(loggerWrapper, httpClientWrapper, this.OpenApiServiceProfile, this.OpenApiSpectralRulesetUrl);
var spectralInstaller = new SpectralInstaller(loggerWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper);

var ciReportRenderer = this.InitializeCiReportRenderer();
var spectralManager = new SpectralRunner(loggerWrapper, processWrapper, diffCalculator, ciReportRenderer, this.OpenApiToolsDirectoryPath, reportsPath);
var oasdiffManager = new OasdiffManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper);
var specGeneratorManager = new SpecGeneratorManager(loggerWrapper);

var generateContractProcess = new GenerateContractProcess(loggerWrapper, spectralInstaller, spectralRulesetManager, spectralManager, swaggerManager, specGeneratorManager, oasdiffManager);
var validateContractProcess = new ValidateContractProcess(loggerWrapper, spectralInstaller, spectralRulesetManager, spectralManager, swaggerManager, oasdiffManager);

try
{
await this.GeneratePublicNugetSource();
Expand Down Expand Up @@ -161,4 +177,16 @@ private async Task GeneratePublicNugetSource()
File.WriteAllText(path, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n <clear />\n <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\n </packageSources>\n</configuration>");
}
}

// In the future, we will want to support both ADO and Github CI environments
private AdoCiReportRenderer InitializeCiReportRenderer()
{
switch (this.OpenApiCiReportEnvironment)
{
case Ado:
return new AdoCiReportRenderer();
default:
return new AdoCiReportRenderer();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<!-- We use this property when developing this MSBuild task in an IDE, -->
<!-- so we can reference the task assembly that is built in its bin folder instead of the one in the distributed NuGet package -->
<OpenApiDebuggingEnabled Condition="'$(OpenApiDebuggingEnabled)' == ''">false</OpenApiDebuggingEnabled>
<!-- We use this property to determine the format for spectral report exports for the CI environments -->
<OpenApiCiReportEnvironment Condition="'$(OpenApiCiReportEnvironment)' == ''">ado</OpenApiCiReportEnvironment>
<OpenApiTouchFileName>openapi_spec_validated.txt</OpenApiTouchFileName>
</PropertyGroup>

Expand Down Expand Up @@ -83,6 +85,7 @@
OpenApiSpecificationFiles="$(OpenApiSpecificationFiles)"
OpenApiTreatWarningsAsErrors="$(OpenApiTreatWarningsAsErrors)"
OpenApiServiceProfile="$(OpenApiServiceProfile)"
OpenApiCiReportEnvironment="$(OpenApiCiReportEnvironment)"
/>
<Touch Files="$(IntermediateOutputPath)$(OpenApiTouchFileName)" AlwaysCreate="true" />
</Target>
Expand Down

0 comments on commit 8b55e6f

Please sign in to comment.