Skip to content

Commit f2bbe27

Browse files
committed
working
1 parent 5876eee commit f2bbe27

18 files changed

+477
-9
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections;
5+
using PSRule.Definitions;
6+
using PSRule.Definitions.Conventions;
7+
using PSRule.Runtime;
8+
9+
namespace PSRule.Examples;
10+
11+
/// <summary>
12+
/// Example convention that demonstrates how to contribute content to job summaries.
13+
/// This shows how convention authors can implement IJobSummaryContributor to extend job summary output.
14+
/// </summary>
15+
internal sealed class ExampleJobSummaryConvention : BaseConvention, IConventionV1, IJobSummaryContributor
16+
{
17+
private int _processedCount = 0;
18+
private int _successCount = 0;
19+
20+
public ExampleJobSummaryConvention(ISourceFile source, string name) : base(source, name)
21+
{
22+
}
23+
24+
public override void Process(LegacyRunspaceContext context, IEnumerable input)
25+
{
26+
// Example: Track some metrics during processing
27+
foreach (var item in input)
28+
{
29+
_processedCount++;
30+
// Simulate some processing logic
31+
if (_processedCount % 2 == 0)
32+
_successCount++;
33+
}
34+
}
35+
36+
public IEnumerable<JobSummarySection>? GetJobSummaryContent()
37+
{
38+
// Return custom sections for the job summary
39+
var sections = new List<JobSummarySection>();
40+
41+
// Add a metrics section
42+
var metricsContent = $@"The example convention processed {_processedCount} items with {_successCount} successful operations.
43+
44+
### Breakdown
45+
- Total items: {_processedCount}
46+
- Successful: {_successCount}
47+
- Success rate: {(_processedCount > 0 ? (_successCount * 100.0 / _processedCount):0):F1}%";
48+
49+
sections.Add(new JobSummarySection("Convention Metrics", new InfoString(metricsContent)));
50+
51+
// Add an additional information section
52+
var additionalInfo = @"This section demonstrates how conventions can contribute custom content to job summaries.
53+
54+
Convention authors can:
55+
- Add custom metrics and statistics
56+
- Provide configuration summaries
57+
- Include environment information
58+
- Display custom analysis results
59+
60+
For more information, see the [PSRule conventions documentation](https://microsoft.github.io/PSRule/concepts/conventions/).";
61+
62+
sections.Add(new JobSummarySection("Convention Information", new InfoString(additionalInfo)));
63+
64+
return sections;
65+
}
66+
67+
#region IConventionV1 implementation (required but not relevant for this example)
68+
69+
public IResourceHelpInfo Info => throw new NotImplementedException();
70+
public ResourceFlags Flags => ResourceFlags.None;
71+
public ISourceExtent Extent => throw new NotImplementedException();
72+
public ResourceKind Kind => ResourceKind.Convention;
73+
public string ApiVersion => "v1";
74+
public ResourceId? Ref => null;
75+
public ResourceId[]? Alias => null;
76+
public IResourceTags? Tags => null;
77+
public IResourceLabels? Labels => null;
78+
79+
#endregion
80+
}

examples/JobSummaryContribution.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# PSRule Job Summary Convention Extension Example
2+
3+
This example demonstrates how conventions can contribute additional information to PSRule job summaries.
4+
5+
## Overview
6+
7+
The new `IJobSummaryContributor` interface allows conventions to append custom sections to job summary output. This enables:
8+
9+
- Custom metrics and statistics
10+
- Environment information
11+
- Configuration summaries
12+
- Additional analysis results
13+
14+
## Usage
15+
16+
Conventions can implement the `IJobSummaryContributor` interface to contribute content:
17+
18+
```csharp
19+
public class MyConvention : BaseConvention, IConventionV1, IJobSummaryContributor
20+
{
21+
public IEnumerable<JobSummarySection>? GetJobSummaryContent()
22+
{
23+
return new[]
24+
{
25+
new JobSummarySection("Custom Metrics", "- Processed: 100 items\n- Success rate: 95%"),
26+
new JobSummarySection("Environment", "- OS: Windows\n- Runtime: .NET 8.0")
27+
};
28+
}
29+
30+
// ... other convention implementation
31+
}
32+
```
33+
34+
## Example Output
35+
36+
The job summary will include additional sections after the standard PSRule content:
37+
38+
```markdown
39+
# PSRule result summary
40+
41+
❌ PSRule completed with an overall result of 'Fail' with 10 rule(s) and 5 target(s) in 00:00:02.123.
42+
43+
## Analysis
44+
...
45+
46+
## Custom Metrics
47+
- Processed: 100 items
48+
- Success rate: 95%
49+
50+
## Environment
51+
- OS: Windows
52+
- Runtime: .NET 8.0
53+
```
54+
55+
## Backward Compatibility
56+
57+
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.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace PSRule.Runtime;
5+
6+
/// <summary>
7+
/// An interface for context available while executing a convention.
8+
/// </summary>
9+
public interface IConventionContext
10+
{
11+
/// <summary>
12+
/// A collection of items to add to the run summary.
13+
/// </summary>
14+
SummaryCollection Summary { get; }
15+
}

src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,11 @@ void AddService<TInterface, TService>()
3333
/// <param name="instanceName">A unique name of the service instance.</param>
3434
/// <param name="instance">An instance of the service.</param>
3535
void AddService(string instanceName, object instance);
36+
37+
/// <summary>
38+
/// Add a convention.
39+
/// </summary>
40+
/// <typeparam name="TConvention">The convention type to add.</typeparam>
41+
void AddConvention<TConvention>()
42+
where TConvention : class;
3643
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace PSRule.Definitions;
5+
6+
/// <summary>
7+
/// Defines an interface for conventions that can contribute additional information to job summaries.
8+
/// </summary>
9+
public interface IJobSummaryContributor
10+
{
11+
/// <summary>
12+
/// Gets additional content to include in the job summary.
13+
/// This method is called during job summary generation to collect custom content from conventions.
14+
/// </summary>
15+
/// <returns>
16+
/// A collection of job summary sections, where each section contains a title and content.
17+
/// The content can be formatted differently for different output formats (plain text for console, markdown for files).
18+
/// Returns null or empty collection if no additional content should be added.
19+
/// </returns>
20+
IEnumerable<JobSummarySection>? GetJobSummaryContent();
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace PSRule.Definitions;
5+
6+
/// <summary>
7+
/// Represents a section of content to be included in a job summary.
8+
/// </summary>
9+
/// <param name="title">The title for the section (will be rendered as H2 header).</param>
10+
/// <param name="content">The content for the section.</param>
11+
public sealed class JobSummarySection(string title, InfoString content)
12+
{
13+
/// <summary>
14+
/// Gets the title for this section.
15+
/// </summary>
16+
public string Title { get; } = title ?? throw new ArgumentNullException(nameof(title));
17+
18+
/// <summary>
19+
/// Gets the content for this section.
20+
/// </summary>
21+
public InfoString Content { get; } = content ?? throw new ArgumentNullException(nameof(content));
22+
}

src/PSRule/PSRule.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
</ItemGroup>
3434

3535
<PropertyGroup>
36-
<PSRule_Version>$(version)</PSRule_Version>
36+
<PSRule_Version Condition="'$(PSRule_Version)' == ''">0.0.1</PSRule_Version>
3737
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
3838
</PropertyGroup>
3939

src/PSRule/Pipeline/AssertPipelineBuilder.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using PSRule.Configuration;
5+
using PSRule.Definitions;
56
using PSRule.Pipeline.Output;
67
using PSRule.Rules;
78

@@ -59,10 +60,13 @@ private bool ShouldOutput()
5960
if (!RequireModules() || !RequireWorkspaceCapabilities() || !RequireSources())
6061
return null;
6162

62-
var context = PrepareContext(PipelineHookActions.Default, writer: HandleJobSummary(writer ?? PrepareWriter()), checkModuleCapabilities: true);
63+
var context = PrepareContext(PipelineHookActions.Default, writer: writer, checkModuleCapabilities: true);
6364
if (context == null)
6465
return null;
6566

67+
// Update job summary writer to include conventions
68+
writer = HandleJobSummary(writer, context);
69+
6670
return new InvokeRulePipeline
6771
(
6872
context: context,
@@ -71,17 +75,21 @@ private bool ShouldOutput()
7175
);
7276
}
7377

74-
private IPipelineWriter HandleJobSummary(IPipelineWriter writer)
78+
private IPipelineWriter HandleJobSummary(IPipelineWriter writer, PipelineContext context)
7579
{
7680
if (string.IsNullOrEmpty(Option.Output.JobSummaryPath))
7781
return writer;
7882

83+
// Get conventions that contribute to job summaries
84+
var contributors = context.ResourceCache.OfType<IJobSummaryContributor>().ToArray();
85+
7986
return new JobSummaryWriter
8087
(
8188
inner: writer,
8289
option: Option,
8390
shouldProcess: ShouldProcess,
84-
source: Source
91+
source: Source,
92+
contributors: contributors.Length > 0 ? contributors : null
8593
);
8694
}
8795
}

src/PSRule/Pipeline/Output/JobSummaryWriter.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Text;
55
using PSRule.Configuration;
6+
using PSRule.Definitions;
67
using PSRule.Resources;
78
using PSRule.Rules;
89

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

2729
private Stream? _Stream;
28-
private StreamWriter _Writer;
30+
private StreamWriter? _Writer;
2931
private bool _IsDisposed;
3032

31-
public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null)
33+
public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null, IJobSummaryContributor[]? contributors = null)
3234
: base(inner, option, shouldProcess)
3335
{
3436
_OutputPath = outputPath ?? Environment.GetRootedPath(Option.Output.JobSummaryPath);
3537
_Encoding = option.Output.GetEncoding();
3638
_JobSummary = JobSummaryFormat.Default;
3739
_Stream = stream;
3840
_Source = source;
41+
_Contributors = contributors;
3942

4043
if (Option.Output.As == ResultFormat.Summary && inner != null)
4144
inner.WriteError(new PipelineConfigurationException("Output.As", PSRuleResources.PSR0002), "PSRule.Output.AsOutputSerialization", System.Management.Automation.ErrorCategory.InvalidOperation);
@@ -135,6 +138,47 @@ private void Complete()
135138
FinalResult(results);
136139
Source();
137140
Analysis(results);
141+
AdditionalInformation();
142+
}
143+
144+
private void AdditionalInformation()
145+
{
146+
if (_Contributors == null || _Contributors.Length == 0)
147+
return;
148+
149+
var sections = new List<JobSummarySection>();
150+
151+
// Collect content from all contributors
152+
for (var i = 0; i < _Contributors.Length; i++)
153+
{
154+
try
155+
{
156+
var contributorSections = _Contributors[i].GetJobSummaryContent();
157+
if (contributorSections != null)
158+
{
159+
sections.AddRange(contributorSections);
160+
}
161+
}
162+
catch
163+
{
164+
// Ignore exceptions from individual contributors to prevent them from breaking the entire job summary
165+
continue;
166+
}
167+
}
168+
169+
// Write sections if any content was provided
170+
if (sections.Count > 0)
171+
{
172+
foreach (var section in sections)
173+
{
174+
if (!string.IsNullOrWhiteSpace(section.Title) && section.Content != null && section.Content.HasValue)
175+
{
176+
H2(section.Title);
177+
WriteLine(section.Content.Markdown);
178+
WriteLine();
179+
}
180+
}
181+
}
138182
}
139183

140184
private void Analysis(InvokeResult[] o)

src/PSRule/Pipeline/ResultOutputWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal abstract class ResultOutputWriter<T> : PipelineWriter
1313
protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess)
1414
: base(inner, option, shouldProcess)
1515
{
16-
_Result = new List<T>();
16+
_Result = [];
1717
}
1818

1919
public override void WriteObject(object sendToPipeline, bool enumerateCollection)

0 commit comments

Comments
 (0)