From 8505c75b152b46e3de97f7df9f3718c14ec8057c Mon Sep 17 00:00:00 2001 From: Tim Schneider <43130816+DerStimmler@users.noreply.github.com> Date: Mon, 25 Aug 2025 00:31:23 +0200 Subject: [PATCH 1/3] perf: reuse IResultErrorMapper instances --- .../ToAcceptedAtRouteHttpResultTE.cs | 4 +- .../ToAcceptedHttpResultTE.cs | 4 +- .../ToContentHttpResultStringE.cs | 2 +- .../ToCreatedAtRouteHttpResultTE.cs | 4 +- .../ResultExtensions/ToCreatedHttpResultTE.cs | 2 +- .../ToFileHttpResultByteArrayE.cs | 4 +- .../ToFileStreamHttpResultStreamE.cs | 4 +- .../ResultExtensions/ToJsonHttpResultTE.cs | 4 +- .../ToNoContentHttpResultTE.cs | 4 +- .../ResultExtensions/ToOkHttpResultTE.cs | 4 +- .../ToStatusCodeHttpResultTE.cs | 4 +- .../ResultExtensionsGenerator.cs | 37 +++++++++++++++++++ .../ToNoContentHttpResultE.cs | 4 +- .../ToStatusCodeHttpResultE.cs | 4 +- 14 files changed, 61 insertions(+), 24 deletions(-) diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedAtRouteHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedAtRouteHttpResultTE.cs index a743286..c18526c 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedAtRouteHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedAtRouteHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results, {{httpResultType}}> ToAcceptedAtRouteHttpResult(this Result result, string? routeName = null, Func? routeValues = null) { if (result.IsSuccess) return TypedResults.AcceptedAtRoute(result.Value, routeName, routeValues?.Invoke(result.Value)); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedHttpResultTE.cs index a12fcf6..fe71561 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToAcceptedHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results, {{httpResultType}}> ToAcceptedHttpResult(this Result result, Func uri) { if (result.IsSuccess) return TypedResults.Accepted(uri(result.Value), result.Value); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToContentHttpResultStringE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToContentHttpResultStringE.cs index 47cddb9..906738c 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToContentHttpResultStringE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToContentHttpResultStringE.cs @@ -12,7 +12,7 @@ public string Generate(string mapperClassName, string resultErrorType, string ht { if (result.IsSuccess) return TypedResults.Content(result.Value, contentType, contentEncoding, statusCode); - return new {{mapperClassName}}().Map(result.Error); + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedAtRouteHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedAtRouteHttpResultTE.cs index c10c879..3b5c8ab 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedAtRouteHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedAtRouteHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results, {{httpResultType}}> ToCreatedAtRouteHttpResult(this Result result, string? routeName = null, Func? routeValues = null) { if (result.IsSuccess) return TypedResults.CreatedAtRoute(result.Value, routeName, routeValues?.Invoke(result.Value)); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedHttpResultTE.cs index a8ced8c..f2a98b6 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToCreatedHttpResultTE.cs @@ -15,7 +15,7 @@ public string Generate(string mapperClassName, string resultErrorType, string ht ? TypedResults.Created(string.Empty, result.Value) : TypedResults.Created(uri.Invoke(result.Value), result.Value); - return new {{mapperClassName}}().Map(result.Error); + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileHttpResultByteArrayE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileHttpResultByteArrayE.cs index 01a3b6b..2f5052f 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileHttpResultByteArrayE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileHttpResultByteArrayE.cs @@ -14,8 +14,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht bool enableRangeProcessing = false) { if (result.IsSuccess) return TypedResults.File(result.Value, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileStreamHttpResultStreamE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileStreamHttpResultStreamE.cs index 7653832..fb41e69 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileStreamHttpResultStreamE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToFileStreamHttpResultStreamE.cs @@ -14,8 +14,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht bool enableRangeProcessing = false) where T : Stream { if (result.IsSuccess) return TypedResults.Stream(result.Value, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToJsonHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToJsonHttpResultTE.cs index cbb423c..857c1f0 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToJsonHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToJsonHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results, {{httpResultType}}> ToJsonHttpResult(this Result result, int successStatusCode = 200) { if (result.IsSuccess) return TypedResults.Json(result.Value, statusCode: successStatusCode); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToNoContentHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToNoContentHttpResultTE.cs index 2ad7f32..f9944ab 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToNoContentHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToNoContentHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results ToNoContentHttpResult(this Result result) { if (result.IsSuccess) return TypedResults.NoContent(); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToOkHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToOkHttpResultTE.cs index f21d08c..8a3e723 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToOkHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToOkHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results, {{httpResultType}}> ToOkHttpResult(this Result result) { if (result.IsSuccess) return TypedResults.Ok(result.Value); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToStatusCodeHttpResultTE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToStatusCodeHttpResultTE.cs index 6de6fab..ee59d4f 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToStatusCodeHttpResultTE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensions/ToStatusCodeHttpResultTE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results ToStatusCodeHttpResult(this Result result, int successStatusCode = 204) { if (result.IsSuccess) return TypedResults.StatusCode(successStatusCode); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGenerator.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGenerator.cs index 79c400b..5313fd8 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGenerator.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGenerator.cs @@ -64,6 +64,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (!ResultExtensionsGeneratorValidator.CheckRules(mapperClasses, context)) return; + var (fileName, sourceText) = CreateErrorMapperInstancesClass(mapperClasses, requiredNamespaces); + context.AddSource(fileName, SourceText.From(sourceText, Encoding.UTF8)); + var classBuilders = new List { new ResultExtensionsClassBuilder(requiredNamespaces, mapperClasses), @@ -76,6 +79,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ); } + /// + /// Creates a class to get singleton instances of the various + /// + private static (string FileName, string SourceText) CreateErrorMapperInstancesClass( + List mapperClasses, + HashSet requiredNamespaces + ) + { + var sourceBuilder = new StringBuilder(); + + sourceBuilder.AppendLine("// "); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("#nullable enable"); + sourceBuilder.AppendLine(); + requiredNamespaces + .Where(@namespace => !@namespace.StartsWith("global")) + .Distinct() + .Select(@namespace => $"using {@namespace};") + .ToList() + .ForEach(@using => sourceBuilder.AppendLine(@using)); + sourceBuilder.AppendLine(); + + sourceBuilder.AppendLine("public static class ErrorMapperInstances {"); + + foreach (var mapperName in mapperClasses) + sourceBuilder.AppendLine( + $" public static {mapperName.Identifier.Text} {mapperName.Identifier.Text} {{ get; }} = new();" + ); + + sourceBuilder.AppendLine("}"); + + return ("ErrorMapperInstances.g.cs", sourceBuilder.ToString()); + } + /// /// Checks if a class implements the interface. /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToNoContentHttpResultE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToNoContentHttpResultE.cs index 4183db7..4e5b114 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToNoContentHttpResultE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToNoContentHttpResultE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results ToNoContentHttpResult(this UnitResult<{{resultErrorType}}> result) { if (result.IsSuccess) return TypedResults.NoContent(); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToStatusCodeHttpResultE.cs b/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToStatusCodeHttpResultE.cs index d01ab36..026b320 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToStatusCodeHttpResultE.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/UnitResultExtensions/ToStatusCodeHttpResultE.cs @@ -11,8 +11,8 @@ public string Generate(string mapperClassName, string resultErrorType, string ht public static Results ToStatusCodeHttpResult(this UnitResult<{{resultErrorType}}> result, int successStatusCode = 204) { if (result.IsSuccess) return TypedResults.StatusCode(successStatusCode); - - return new {{mapperClassName}}().Map(result.Error); + + return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error); } /// From 5da69a3f50c254c0a827065934c7a3c17971fe24 Mon Sep 17 00:00:00 2001 From: Tim Schneider <43130816+DerStimmler@users.noreply.github.com> Date: Mon, 25 Aug 2025 01:01:19 +0200 Subject: [PATCH 2/3] feat: add analyzer to check for parameterless constructor in mapper class --- .../ParameterlessConstructorRuleTests.cs | 60 +++++++++++++++++++ .../AnalyzerReleases.Shipped.md | 8 +++ .../ResultExtensionsGeneratorValidator.cs | 2 +- .../Rules/ParameterlessConstructorRule.cs | 45 ++++++++++++++ README.md | 4 +- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 CSharpFunctionalExtensions.HttpResults.Generators.Tests/Rules/ParameterlessConstructorRuleTests.cs create mode 100644 CSharpFunctionalExtensions.HttpResults.Generators/Rules/ParameterlessConstructorRule.cs diff --git a/CSharpFunctionalExtensions.HttpResults.Generators.Tests/Rules/ParameterlessConstructorRuleTests.cs b/CSharpFunctionalExtensions.HttpResults.Generators.Tests/Rules/ParameterlessConstructorRuleTests.cs new file mode 100644 index 0000000..9eec9b3 --- /dev/null +++ b/CSharpFunctionalExtensions.HttpResults.Generators.Tests/Rules/ParameterlessConstructorRuleTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Microsoft.CodeAnalysis; + +namespace CSharpFunctionalExtensions.HttpResults.Generators.Tests.Rules; + +public class ParameterlessConstructorRuleTests +{ + [Fact] + public void TestParameterlessConstructor() + { + var sourceCode = """ + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.HttpResults; + using CSharpFunctionalExtensions.HttpResults; + + public class DocumentCreationError + { + public required string DocumentId { get; init; } + } + + // Not parameterless --> error + public class DocumentCreationErrorMapper : IResultErrorMapper> + { + public DocumentCreationErrorMapper(string foo) { } + + public Conflict Map(DocumentCreationError error) => TypedResults.Conflict(error.DocumentId); + } + + // Explicit parameterless --> No error + public class DocumentCreationErrorMapper2 : IResultErrorMapper> + { + public DocumentCreationErrorMapper2() { } + + public Conflict Map(string error) => TypedResults.Conflict(error.DocumentId); + } + + // Explicit parameterless & with parameters --> No error + public class DocumentCreationErrorMapper3 : IResultErrorMapper> + { + public DocumentCreationErrorMapper3() { } + public DocumentCreationErrorMapper3(string foo) { } + + public Conflict Map(int error) => TypedResults.Conflict(error.DocumentId); + } + """; + + var diagnostics = ResultExtensionsGeneratorTestHelper.RunGenerator(sourceCode).ToList(); + + diagnostics.Count.Should().Be(1); + + var diagnostic = diagnostics[0]; + + diagnostic.Id.Should().Be("CFEHTTPR004"); + diagnostic.Severity.Should().Be(DiagnosticSeverity.Error); + diagnostic + .GetMessage() + .Should() + .Be("Class 'DocumentCreationErrorMapper' does not have a parameterless constructor"); + } +} diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/AnalyzerReleases.Shipped.md b/CSharpFunctionalExtensions.HttpResults.Generators/AnalyzerReleases.Shipped.md index 7746192..068e2b2 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/AnalyzerReleases.Shipped.md +++ b/CSharpFunctionalExtensions.HttpResults.Generators/AnalyzerReleases.Shipped.md @@ -44,3 +44,11 @@ | Rule ID | Category | Severity | Notes | |-------------|----------|----------|----------------------------------------| | CFEHTTPR003 | Mapping | Error | Empty Map getter in IResultErrorMapper | + +## Release v1.0.1 + +### New Rules + +| Rule ID | Category | Severity | Notes | +|-------------|----------|----------|---------------------------------------------------------| +| CFEHTTPR004 | Mapping | Error | Missing parameterless constructor in IResultErrorMapper | diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGeneratorValidator.cs b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGeneratorValidator.cs index d2ef140..15429eb 100644 --- a/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGeneratorValidator.cs +++ b/CSharpFunctionalExtensions.HttpResults.Generators/ResultExtensionsGeneratorValidator.cs @@ -9,7 +9,7 @@ namespace CSharpFunctionalExtensions.HttpResults.Generators; /// internal static class ResultExtensionsGeneratorValidator { - private static readonly List Rules = [new DuplicateMapperRule()]; + private static readonly List Rules = [new DuplicateMapperRule(), new ParameterlessConstructorRule()]; /// /// Validates the rules for the generator. diff --git a/CSharpFunctionalExtensions.HttpResults.Generators/Rules/ParameterlessConstructorRule.cs b/CSharpFunctionalExtensions.HttpResults.Generators/Rules/ParameterlessConstructorRule.cs new file mode 100644 index 0000000..135d8ce --- /dev/null +++ b/CSharpFunctionalExtensions.HttpResults.Generators/Rules/ParameterlessConstructorRule.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CSharpFunctionalExtensions.HttpResults.Generators.Rules; + +internal class ParameterlessConstructorRule : IRule +{ + public DiagnosticDescriptor RuleDescriptor { get; } = + new( + "CFEHTTPR004", + "Missing parameterless constructor in IResultErrorMapper", + "Class '{0}' does not have a parameterless constructor", + "Mapping", + DiagnosticSeverity.Error, + true, + customTags: ["CompilationEnd"] + ); + + public IEnumerable Check(List mapperClasses) + { + foreach (var mapperClass in mapperClasses) + { + if (!HasParameterlessConstructor(mapperClass)) + yield return Diagnostic.Create( + RuleDescriptor, + mapperClass.Identifier.GetLocation(), + mapperClass.Identifier.Text + ); + } + } + + private static bool HasParameterlessConstructor(ClassDeclarationSyntax classDeclaration) + { + var hasExplicitParameterless = classDeclaration + .Members.OfType() + .Any(c => c.ParameterList.Parameters.Count == 0); + + if (hasExplicitParameterless) + return true; + + var hasAnyConstructors = classDeclaration.Members.OfType().Any(); + + return !hasAnyConstructors; + } +} diff --git a/README.md b/README.md index d20cdd6..a69afbe 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ When using `Result` or `UnitResult`, this library uses a Source Generato }; } ``` -3. Use the auto generated extension method: +3. Use the auto-generated extension method: ```csharp app.MapGet("/users/{id}", (string id, UserRepository repo) => repo.Find(id) //Result @@ -222,7 +222,7 @@ When using `Result` or `UnitResult`, this library uses a Source Generato This library includes analyzers to help you use it correctly. -For example, they will notify you if you have multiple mappers for the same custom error type. +For example, they will notify you if you have multiple mappers for the same custom error type or if your mapper class doesn't have a parameterless constructor. You can find a complete list of all analyzers [here](https://github.com/co-IT/CSharpFunctionalExtensions.HttpResults/blob/main/CSharpFunctionalExtensions.HttpResults.Generators/AnalyzerReleases.Shipped.md). From d6403e8168e05017e1b56a038d3041e00b79ef4e Mon Sep 17 00:00:00 2001 From: Tim Schneider <43130816+DerStimmler@users.noreply.github.com> Date: Mon, 25 Aug 2025 01:18:24 +0200 Subject: [PATCH 3/3] ci: disable lychee fragment checks temporarily --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4edbbc5..d37f70d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -52,6 +52,6 @@ jobs: uses: lycheeverse/lychee-action@v2 with: fail: true - args: --remap '${{ github.event.repository.default_branch }} ${{ github.head_ref || github.ref_name }}' --include-fragments . + args: --remap '${{ github.event.repository.default_branch }} ${{ github.head_ref || github.ref_name }}' . jobSummary: true format: markdown