Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/ExampleJobSummaryConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections;
using PSRule.Definitions;
using PSRule.Definitions.Conventions;
using PSRule.Runtime;

namespace PSRule.Examples;

/// <summary>
/// Example convention that demonstrates how to contribute content to job summaries.
/// This shows how convention authors can implement IJobSummaryContributor to extend job summary output.
/// </summary>
internal sealed class ExampleJobSummaryConvention : BaseConvention, IConventionV1, IJobSummaryContributor
{
private int _processedCount = 0;
private int _successCount = 0;

public ExampleJobSummaryConvention(ISourceFile source, string name) : base(source, name)
{
}

public override void Process(LegacyRunspaceContext context, IEnumerable input)
{
// Example: Track some metrics during processing
foreach (var item in input)
{
_processedCount++;
// Simulate some processing logic
if (_processedCount % 2 == 0)
_successCount++;
}
}

public IEnumerable<JobSummarySection>? GetJobSummaryContent()
{
// Return custom sections for the job summary
var sections = new List<JobSummarySection>();

// Add a metrics section
var metricsContent = $@"The example convention processed {_processedCount} items with {_successCount} successful operations.

### Breakdown
- Total items: {_processedCount}
- Successful: {_successCount}
- Success rate: {(_processedCount > 0 ? (_successCount * 100.0 / _processedCount):0):F1}%";

sections.Add(new JobSummarySection("Convention Metrics", new InfoString(metricsContent)));

// Add an additional information section
var additionalInfo = @"This section demonstrates how conventions can contribute custom content to job summaries.

Convention authors can:
- Add custom metrics and statistics
- Provide configuration summaries
- Include environment information
- Display custom analysis results

For more information, see the [PSRule conventions documentation](https://microsoft.github.io/PSRule/concepts/conventions/).";

sections.Add(new JobSummarySection("Convention Information", new InfoString(additionalInfo)));

return sections;
}

#region IConventionV1 implementation (required but not relevant for this example)

public IResourceHelpInfo Info => throw new NotImplementedException();
public ResourceFlags Flags => ResourceFlags.None;
public ISourceExtent Extent => throw new NotImplementedException();
public ResourceKind Kind => ResourceKind.Convention;
public string ApiVersion => "v1";
public ResourceId? Ref => null;
public ResourceId[]? Alias => null;
public IResourceTags? Tags => null;
public IResourceLabels? Labels => null;

#endregion
}
57 changes: 57 additions & 0 deletions examples/JobSummaryContribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# PSRule Job Summary Convention Extension Example

This example demonstrates how conventions can contribute additional information to PSRule job summaries.

## Overview

The new `IJobSummaryContributor` interface allows conventions to append custom sections to job summary output. This enables:

- Custom metrics and statistics
- Environment information
- Configuration summaries
- Additional analysis results

## Usage

Conventions can implement the `IJobSummaryContributor` interface to contribute content:

```csharp
public class MyConvention : BaseConvention, IConventionV1, IJobSummaryContributor
{
public IEnumerable<JobSummarySection>? GetJobSummaryContent()
{
return new[]
{
new JobSummarySection("Custom Metrics", "- Processed: 100 items\n- Success rate: 95%"),
new JobSummarySection("Environment", "- OS: Windows\n- Runtime: .NET 8.0")
};
}

// ... other convention implementation
}
```

## Example Output

The job summary will include additional sections after the standard PSRule content:

```markdown
# PSRule result summary

❌ PSRule completed with an overall result of 'Fail' with 10 rule(s) and 5 target(s) in 00:00:02.123.

## Analysis
...

## Custom Metrics
- Processed: 100 items
- Success rate: 95%

## Environment
- OS: Windows
- Runtime: .NET 8.0
```

## Backward Compatibility

This feature is fully backward compatible. Existing conventions and job summaries will continue to work unchanged. Only conventions that explicitly implement `IJobSummaryContributor` will contribute additional content.
15 changes: 15 additions & 0 deletions src/PSRule.Types/Runtime/IConventionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// An interface for context available while executing a convention.
/// </summary>
public interface IConventionContext
{
/// <summary>
/// A collection of items to add to the run summary.
/// </summary>
SummaryCollection Summary { get; }

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build module

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build module

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build module

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build module

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build module

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build extension

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 14 in src/PSRule.Types/Runtime/IConventionContext.cs

View workflow job for this annotation

GitHub Actions / Build extension

The type or namespace name 'SummaryCollection' could not be found (are you missing a using directive or an assembly reference?)
}
7 changes: 7 additions & 0 deletions src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ void AddService<TInterface, TService>()
/// <param name="instanceName">A unique name of the service instance.</param>
/// <param name="instance">An instance of the service.</param>
void AddService(string instanceName, object instance);

/// <summary>
/// Add a convention.
/// </summary>
/// <typeparam name="TConvention">The convention type to add.</typeparam>
void AddConvention<TConvention>()
where TConvention : class;
}
21 changes: 21 additions & 0 deletions src/PSRule/Definitions/IJobSummaryContributor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Definitions;

/// <summary>
/// Defines an interface for conventions that can contribute additional information to job summaries.
/// </summary>
public interface IJobSummaryContributor
{
/// <summary>
/// Gets additional content to include in the job summary.
/// This method is called during job summary generation to collect custom content from conventions.
/// </summary>
/// <returns>
/// A collection of job summary sections, where each section contains a title and content.
/// The content can be formatted differently for different output formats (plain text for console, markdown for files).
/// Returns null or empty collection if no additional content should be added.
/// </returns>
IEnumerable<JobSummarySection>? GetJobSummaryContent();
}
22 changes: 22 additions & 0 deletions src/PSRule/Definitions/JobSummarySection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Definitions;

/// <summary>
/// Represents a section of content to be included in a job summary.
/// </summary>
/// <param name="title">The title for the section (will be rendered as H2 header).</param>
/// <param name="content">The content for the section.</param>
public sealed class JobSummarySection(string title, InfoString content)
{
/// <summary>
/// Gets the title for this section.
/// </summary>
public string Title { get; } = title ?? throw new ArgumentNullException(nameof(title));

/// <summary>
/// Gets the content for this section.
/// </summary>
public InfoString Content { get; } = content ?? throw new ArgumentNullException(nameof(content));
}
2 changes: 1 addition & 1 deletion src/PSRule/PSRule.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</ItemGroup>

<PropertyGroup>
<PSRule_Version>$(version)</PSRule_Version>
<PSRule_Version Condition="'$(PSRule_Version)' == ''">0.0.1</PSRule_Version>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Expand Down
14 changes: 11 additions & 3 deletions src/PSRule/Pipeline/AssertPipelineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using PSRule.Configuration;
using PSRule.Definitions;
using PSRule.Pipeline.Output;
using PSRule.Rules;

Expand Down Expand Up @@ -59,10 +60,13 @@ private bool ShouldOutput()
if (!RequireModules() || !RequireWorkspaceCapabilities() || !RequireSources())
return null;

var context = PrepareContext(PipelineHookActions.Default, writer: HandleJobSummary(writer ?? PrepareWriter()), checkModuleCapabilities: true);
var context = PrepareContext(PipelineHookActions.Default, writer: writer, checkModuleCapabilities: true);
if (context == null)
return null;

// Update job summary writer to include conventions
writer = HandleJobSummary(writer, context);

return new InvokeRulePipeline
(
context: context,
Expand All @@ -71,17 +75,21 @@ private bool ShouldOutput()
);
}

private IPipelineWriter HandleJobSummary(IPipelineWriter writer)
private IPipelineWriter HandleJobSummary(IPipelineWriter writer, PipelineContext context)
{
if (string.IsNullOrEmpty(Option.Output.JobSummaryPath))
return writer;

// Get conventions that contribute to job summaries
var contributors = context.ResourceCache.OfType<IJobSummaryContributor>().ToArray();

return new JobSummaryWriter
(
inner: writer,
option: Option,
shouldProcess: ShouldProcess,
source: Source
source: Source,
contributors: contributors.Length > 0 ? contributors : null
);
}
}
48 changes: 46 additions & 2 deletions src/PSRule/Pipeline/Output/JobSummaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text;
using PSRule.Configuration;
using PSRule.Definitions;
using PSRule.Resources;
using PSRule.Rules;

Expand All @@ -23,19 +24,21 @@ internal sealed class JobSummaryWriter : ResultOutputWriter<InvokeResult>
private readonly Encoding _Encoding;
private readonly JobSummaryFormat _JobSummary;
private readonly Source[]? _Source;
private readonly IJobSummaryContributor[]? _Contributors;

private Stream? _Stream;
private StreamWriter _Writer;
private StreamWriter? _Writer;
private bool _IsDisposed;

public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null)
public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null, IJobSummaryContributor[]? contributors = null)
: base(inner, option, shouldProcess)
{
_OutputPath = outputPath ?? Environment.GetRootedPath(Option.Output.JobSummaryPath);
_Encoding = option.Output.GetEncoding();
_JobSummary = JobSummaryFormat.Default;
_Stream = stream;
_Source = source;
_Contributors = contributors;

if (Option.Output.As == ResultFormat.Summary && inner != null)
inner.WriteError(new PipelineConfigurationException("Output.As", PSRuleResources.PSR0002), "PSRule.Output.AsOutputSerialization", System.Management.Automation.ErrorCategory.InvalidOperation);
Expand Down Expand Up @@ -135,6 +138,47 @@ private void Complete()
FinalResult(results);
Source();
Analysis(results);
AdditionalInformation();
}

private void AdditionalInformation()
{
if (_Contributors == null || _Contributors.Length == 0)
return;

var sections = new List<JobSummarySection>();

// Collect content from all contributors
for (var i = 0; i < _Contributors.Length; i++)
{
try
{
var contributorSections = _Contributors[i].GetJobSummaryContent();
if (contributorSections != null)
{
sections.AddRange(contributorSections);
}
}
catch
{
// Ignore exceptions from individual contributors to prevent them from breaking the entire job summary
continue;
}
}

// Write sections if any content was provided
if (sections.Count > 0)
{
foreach (var section in sections)
{
if (!string.IsNullOrWhiteSpace(section.Title) && section.Content != null && section.Content.HasValue)
{
H2(section.Title);
WriteLine(section.Content.Markdown);
WriteLine();
}
}
}
}

private void Analysis(InvokeResult[] o)
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule/Pipeline/ResultOutputWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal abstract class ResultOutputWriter<T> : PipelineWriter
protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess)
: base(inner, option, shouldProcess)
{
_Result = new List<T>();
_Result = [];
}

public override void WriteObject(object sendToPipeline, bool enumerateCollection)
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule/Runtime/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ private static string GetFileType(string value)
/// <summary>
/// Determine line comment prefix by file extension
/// </summary>
private static string DetectLinePrefix(string extension)
private static string DetectLinePrefix(string? extension)
{
extension = extension?.ToLower();
switch (extension)
Expand Down
5 changes: 5 additions & 0 deletions src/PSRule/Runtime/ILanguageScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ internal interface ILanguageScope : IDisposable
/// </summary>
IEnumerable<Type> GetEmitters();

/// <summary>
/// Get any conventions added to the scope.
/// </summary>
IEnumerable<Type> GetConventions();

ITargetBindingResult? Bind(ITargetObject targetObject);

/// <summary>
Expand Down
Loading
Loading