From 0b9e5ab8203663709241f95a819765715f54ed67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:04:24 +0000 Subject: [PATCH 1/7] Initial plan From 333fb36af55d69e9dc1e8378acdf5d1194503a58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:24:57 +0000 Subject: [PATCH 2/7] Add code fix for MSTEST0031 DoNotUseSystemDescriptionAttribute Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/bfc27008-89bf-43d2-8334-80122ee20056 Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../CodeFixResources.resx | 3 + ...DoNotUseSystemDescriptionAttributeFixer.cs | 160 ++++++++++++++++++ .../xlf/CodeFixResources.cs.xlf | 5 + .../xlf/CodeFixResources.de.xlf | 5 + .../xlf/CodeFixResources.es.xlf | 5 + .../xlf/CodeFixResources.fr.xlf | 5 + .../xlf/CodeFixResources.it.xlf | 5 + .../xlf/CodeFixResources.ja.xlf | 5 + .../xlf/CodeFixResources.ko.xlf | 5 + .../xlf/CodeFixResources.pl.xlf | 5 + .../xlf/CodeFixResources.pt-BR.xlf | 5 + .../xlf/CodeFixResources.ru.xlf | 5 + .../xlf/CodeFixResources.tr.xlf | 5 + .../xlf/CodeFixResources.zh-Hans.xlf | 5 + .../xlf/CodeFixResources.zh-Hant.xlf | 5 + ...SystemDescriptionAttributeAnalyzerTests.cs | 52 +++++- 16 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx index e21d53f3df..679cb9c2fd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx @@ -213,4 +213,7 @@ Remove 'out' and 'ref' modifiers + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs new file mode 100644 index 0000000000..88ff9e4988 --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Composition; + +using Analyzer.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +using MSTest.Analyzers.Helpers; + +namespace MSTest.Analyzers; + +/// +/// Code fixer for . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DoNotUseSystemDescriptionAttributeFixer))] +[Shared] +public sealed class DoNotUseSystemDescriptionAttributeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DiagnosticIds.DoNotUseSystemDescriptionAttributeRuleId); + + /// + public override FixAllProvider GetFixAllProvider() + // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + Diagnostic diagnostic = context.Diagnostics[0]; + SyntaxToken syntaxToken = root.FindToken(diagnostic.Location.SourceSpan.Start); + if (syntaxToken.Parent is null) + { + return; + } + + MethodDeclarationSyntax? methodDeclaration = syntaxToken.Parent.AncestorsAndSelf().OfType().FirstOrDefault(); + if (methodDeclaration is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: CodeFixResources.UseTestMethodDisplayNameInsteadOfDescriptionAttributeFix, + createChangedDocument: c => ReplaceDescriptionAttributeAsync(context.Document, methodDeclaration, c), + equivalenceKey: nameof(DoNotUseSystemDescriptionAttributeFixer)), + diagnostic); + } + + private static async Task ReplaceDescriptionAttributeAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + { + SemanticModel semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + INamedTypeSymbol? testMethodAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute); + INamedTypeSymbol? descriptionAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemDescriptionAttribute); + + if (testMethodAttributeSymbol is null || descriptionAttributeSymbol is null) + { + return document; + } + + AttributeSyntax? descriptionAttribute = null; + AttributeSyntax? testMethodAttribute = null; + + foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + if (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol is IMethodSymbol { ContainingType: { } containingType }) + { + if (SymbolEqualityComparer.Default.Equals(containingType, descriptionAttributeSymbol)) + { + descriptionAttribute = attribute; + } + else if (IsOrInheritsFrom(containingType, testMethodAttributeSymbol)) + { + testMethodAttribute = attribute; + } + } + } + } + + if (descriptionAttribute is null || testMethodAttribute is null) + { + return document; + } + + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + // Add DisplayName = "text" to the [TestMethod] attribute (only if it doesn't already have DisplayName) + bool hasDisplayName = testMethodAttribute.ArgumentList?.Arguments.Any( + a => a.NameEquals?.Name.Identifier.ValueText == "DisplayName") == true; + + if (!hasDisplayName && descriptionAttribute.ArgumentList?.Arguments.Count > 0) + { + ExpressionSyntax descriptionExpression = descriptionAttribute.ArgumentList.Arguments[0].Expression; + + AttributeArgumentSyntax displayNameArg = SyntaxFactory.AttributeArgument( + SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("DisplayName")), + nameColon: null, + descriptionExpression); + + AttributeSyntax newTestMethodAttribute = testMethodAttribute.ArgumentList is null + ? testMethodAttribute.WithArgumentList( + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList(displayNameArg))) + : testMethodAttribute.WithArgumentList( + testMethodAttribute.ArgumentList.AddArguments(displayNameArg)); + + editor.ReplaceNode(testMethodAttribute, newTestMethodAttribute); + } + + // Remove the [Description] attribute + if (descriptionAttribute.Parent is AttributeListSyntax containingAttributeList) + { + if (containingAttributeList.Attributes.Count == 1) + { + // Remove the entire attribute list + editor.RemoveNode(containingAttributeList); + } + else + { + // Remove just the attribute from the list + editor.ReplaceNode( + containingAttributeList, + containingAttributeList.RemoveNode(descriptionAttribute, SyntaxRemoveOptions.KeepLeadingTrivia)!); + } + } + + return editor.GetChangedDocument(); + } + + private static bool IsOrInheritsFrom(INamedTypeSymbol? type, INamedTypeSymbol baseType) + { + INamedTypeSymbol? current = type; + while (current is not null) + { + if (SymbolEqualityComparer.Default.Equals(current, baseType)) + { + return true; + } + + current = current.BaseType; + } + + return false; + } +} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf index 68192b0c4e..c1c1a4c778 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf @@ -162,6 +162,11 @@ Použít atribut [OSCondition] + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf index 9e2078ecdf..1d72dc6809 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf @@ -162,6 +162,11 @@ Verwenden des Attributs „[OSCondition]“ + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf index d4f8e03614..38bd95f404 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf @@ -162,6 +162,11 @@ Usar el atributo '[OSCondition]' + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf index 155abbb514..2b6b1cb561 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf @@ -162,6 +162,11 @@ Utiliser l’attribut « [OSCondition] » + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf index 2dff98328e..f73fbd872f 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf @@ -162,6 +162,11 @@ Usa attributo '[OSCondition]' + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf index c99a399364..7d7f1454ee 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf @@ -162,6 +162,11 @@ [OSCondition] 属性を使用する + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf index ea3eee9312..619f8902ec 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf @@ -162,6 +162,11 @@ '[OSCondition]' 특성 사용 + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf index 0004adb3ba..5f2706b80d 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf @@ -162,6 +162,11 @@ Użyj atrybutu „[OSCondition]” + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf index 248ce2ebec..7f755ef4b3 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf @@ -162,6 +162,11 @@ Usar o atributo "[OSCondition]" + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf index f682a3c278..f96191550c 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf @@ -162,6 +162,11 @@ Использовать атрибут "[OSCondition]" + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf index 59df239e3e..75b9cf1f2f 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf @@ -162,6 +162,11 @@ '[OSCondition]' özniteliğini kullanın + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf index a33aa00a87..e576eaba6b 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf @@ -162,6 +162,11 @@ 使用 “[OSCondition]” 属性 + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf index ba0b5661c3..3ca2de84f2 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf @@ -162,6 +162,11 @@ 使用 '[OSCondition]' 屬性 + + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + \ No newline at end of file diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index f927d76c1c..f5104b78d0 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< MSTest.Analyzers.DoNotUseSystemDescriptionAttributeAnalyzer, - Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + MSTest.Analyzers.DoNotUseSystemDescriptionAttributeFixer>; namespace MSTest.Analyzers.Test; @@ -27,7 +27,53 @@ public class MyTestClass } """; - await VerifyCS.VerifyAnalyzerAsync(code); + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod(DisplayName = "Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenTestMethodHasExistingDisplayNameAndSystemDescriptionAttribute_RemovesDescriptionAttribute() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod(DisplayName = "Existing")] + [System.ComponentModel.Description("Description")] + public void [|MyTestMethod|]() + { + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod(DisplayName = "Existing")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } [TestMethod] From aace8a7f7d7de430e1df79f4202cb45ab8daf962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:44:01 +0000 Subject: [PATCH 3/7] Replace with MSTest DescriptionAttribute instead of TestMethod DisplayName Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/8f6b70e9-bcdc-47c6-83a2-e56a7118878c Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../CodeFixResources.resx | 4 +- ...DoNotUseSystemDescriptionAttributeFixer.cs | 100 +++++------------- .../xlf/CodeFixResources.cs.xlf | 10 +- .../xlf/CodeFixResources.de.xlf | 10 +- .../xlf/CodeFixResources.es.xlf | 10 +- .../xlf/CodeFixResources.fr.xlf | 10 +- .../xlf/CodeFixResources.it.xlf | 10 +- .../xlf/CodeFixResources.ja.xlf | 10 +- .../xlf/CodeFixResources.ko.xlf | 10 +- .../xlf/CodeFixResources.pl.xlf | 10 +- .../xlf/CodeFixResources.pt-BR.xlf | 10 +- .../xlf/CodeFixResources.ru.xlf | 10 +- .../xlf/CodeFixResources.tr.xlf | 10 +- .../xlf/CodeFixResources.zh-Hans.xlf | 10 +- .../xlf/CodeFixResources.zh-Hant.xlf | 10 +- ...SystemDescriptionAttributeAnalyzerTests.cs | 36 +------ 16 files changed, 93 insertions(+), 177 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx index 679cb9c2fd..0a93a4d9fd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx @@ -213,7 +213,7 @@ Remove 'out' and 'ref' modifiers - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute + + Use MSTest 'Description' attribute instead \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs index 88ff9e4988..f981a95149 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs @@ -11,7 +11,6 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; using MSTest.Analyzers.Helpers; @@ -53,108 +52,57 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( - title: CodeFixResources.UseTestMethodDisplayNameInsteadOfDescriptionAttributeFix, - createChangedDocument: c => ReplaceDescriptionAttributeAsync(context.Document, methodDeclaration, c), + title: CodeFixResources.UseMSTestDescriptionAttributeInsteadFix, + createChangedDocument: c => ReplaceWithMSTestDescriptionAttributeAsync(context.Document, methodDeclaration, c), equivalenceKey: nameof(DoNotUseSystemDescriptionAttributeFixer)), diagnostic); } - private static async Task ReplaceDescriptionAttributeAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + private static async Task ReplaceWithMSTestDescriptionAttributeAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) { SemanticModel semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - INamedTypeSymbol? testMethodAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute); - INamedTypeSymbol? descriptionAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemDescriptionAttribute); + INamedTypeSymbol? systemDescriptionAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemDescriptionAttribute); - if (testMethodAttributeSymbol is null || descriptionAttributeSymbol is null) + if (systemDescriptionAttributeSymbol is null) { return document; } - AttributeSyntax? descriptionAttribute = null; - AttributeSyntax? testMethodAttribute = null; + AttributeSyntax? systemDescriptionAttribute = null; foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) { foreach (AttributeSyntax attribute in attributeList.Attributes) { - if (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol is IMethodSymbol { ContainingType: { } containingType }) + if (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol is IMethodSymbol { ContainingType: { } containingType } + && SymbolEqualityComparer.Default.Equals(containingType, systemDescriptionAttributeSymbol)) { - if (SymbolEqualityComparer.Default.Equals(containingType, descriptionAttributeSymbol)) - { - descriptionAttribute = attribute; - } - else if (IsOrInheritsFrom(containingType, testMethodAttributeSymbol)) - { - testMethodAttribute = attribute; - } + systemDescriptionAttribute = attribute; + break; } } - } - - if (descriptionAttribute is null || testMethodAttribute is null) - { - return document; - } - - DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - - // Add DisplayName = "text" to the [TestMethod] attribute (only if it doesn't already have DisplayName) - bool hasDisplayName = testMethodAttribute.ArgumentList?.Arguments.Any( - a => a.NameEquals?.Name.Identifier.ValueText == "DisplayName") == true; - - if (!hasDisplayName && descriptionAttribute.ArgumentList?.Arguments.Count > 0) - { - ExpressionSyntax descriptionExpression = descriptionAttribute.ArgumentList.Arguments[0].Expression; - - AttributeArgumentSyntax displayNameArg = SyntaxFactory.AttributeArgument( - SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("DisplayName")), - nameColon: null, - descriptionExpression); - - AttributeSyntax newTestMethodAttribute = testMethodAttribute.ArgumentList is null - ? testMethodAttribute.WithArgumentList( - SyntaxFactory.AttributeArgumentList( - SyntaxFactory.SingletonSeparatedList(displayNameArg))) - : testMethodAttribute.WithArgumentList( - testMethodAttribute.ArgumentList.AddArguments(displayNameArg)); - - editor.ReplaceNode(testMethodAttribute, newTestMethodAttribute); - } - // Remove the [Description] attribute - if (descriptionAttribute.Parent is AttributeListSyntax containingAttributeList) - { - if (containingAttributeList.Attributes.Count == 1) - { - // Remove the entire attribute list - editor.RemoveNode(containingAttributeList); - } - else + if (systemDescriptionAttribute is not null) { - // Remove just the attribute from the list - editor.ReplaceNode( - containingAttributeList, - containingAttributeList.RemoveNode(descriptionAttribute, SyntaxRemoveOptions.KeepLeadingTrivia)!); + break; } } - return editor.GetChangedDocument(); - } - - private static bool IsOrInheritsFrom(INamedTypeSymbol? type, INamedTypeSymbol baseType) - { - INamedTypeSymbol? current = type; - while (current is not null) + if (systemDescriptionAttribute is null) { - if (SymbolEqualityComparer.Default.Equals(current, baseType)) - { - return true; - } - - current = current.BaseType; + return document; } - return false; + // Replace the System.ComponentModel.Description attribute name with the MSTest Description attribute name. + // Since the MSTest namespace (Microsoft.VisualStudio.TestTools.UnitTesting) is already in scope, + // we can use just the simple name "Description" which will resolve to MSTest's DescriptionAttribute. + AttributeSyntax newAttribute = systemDescriptionAttribute.WithName( + SyntaxFactory.IdentifierName("Description") + .WithTriviaFrom(systemDescriptionAttribute.Name)); + + SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + return document.WithSyntaxRoot(root.ReplaceNode(systemDescriptionAttribute, newAttribute)); } } diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf index c1c1a4c778..b61998fbe8 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf @@ -152,6 +152,11 @@ Místo řetězcového argumentu použijte vlastnost DisplayName + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Použít {0} @@ -162,11 +167,6 @@ Použít atribut [OSCondition] - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf index 1d72dc6809..9ad9d303f0 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf @@ -152,6 +152,11 @@ Verwenden Sie die Eigenschaft „DisplayName“ anstelle eines Zeichenfolgenarguments. + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' "{0}" verwenden @@ -162,11 +167,6 @@ Verwenden des Attributs „[OSCondition]“ - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf index 38bd95f404..d708829e76 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf @@ -152,6 +152,11 @@ Usar la propiedad "DisplayName" en lugar del argumento de cadena + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usar "{0}" @@ -162,11 +167,6 @@ Usar el atributo '[OSCondition]' - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf index 2b6b1cb561..eb70dc03d5 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf @@ -152,6 +152,11 @@ Utilisez la propriété « DisplayName » au lieu d’un argument de type chaîne + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Utiliser « {0} » @@ -162,11 +167,6 @@ Utiliser l’attribut « [OSCondition] » - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf index f73fbd872f..bf367d6d70 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf @@ -152,6 +152,11 @@ Usare la proprietà 'DisplayName' invece di un argomento stringa + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usa '{0}' @@ -162,11 +167,6 @@ Usa attributo '[OSCondition]' - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf index 7d7f1454ee..dc3ef77325 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf @@ -152,6 +152,11 @@ 文字列引数の代わりに 'DisplayName' プロパティを使用する + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' を使用します @@ -162,11 +167,6 @@ [OSCondition] 属性を使用する - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf index 619f8902ec..467c2bf340 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf @@ -152,6 +152,11 @@ 문자열 인수 대신 'DisplayName' 속성 사용 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' 사용 @@ -162,11 +167,6 @@ '[OSCondition]' 특성 사용 - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf index 5f2706b80d..a6cd22654f 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf @@ -152,6 +152,11 @@ Użyj właściwości „DisplayName” zamiast argumentu ciągu + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Użyj „{0}” @@ -162,11 +167,6 @@ Użyj atrybutu „[OSCondition]” - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf index 7f755ef4b3..e580cbaee2 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf @@ -152,6 +152,11 @@ Usar a propriedade "DisplayName" em vez do argumento de cadeia de caracteres + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usar '{0}' @@ -162,11 +167,6 @@ Usar o atributo "[OSCondition]" - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf index f96191550c..ded6db441d 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf @@ -152,6 +152,11 @@ Использовать свойство "DisplayName" вместо строкового аргумента + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Использовать "{0}" @@ -162,11 +167,6 @@ Использовать атрибут "[OSCondition]" - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf index 75b9cf1f2f..8188db380e 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf @@ -152,6 +152,11 @@ Dize bağımsız değişkeni yerine 'DisplayName' özelliğini kullanın + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' kullan @@ -162,11 +167,6 @@ '[OSCondition]' özniteliğini kullanın - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf index e576eaba6b..48d95e5157 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf @@ -152,6 +152,11 @@ 使用属性‘DisplayName’替代字符串参数 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' 使用“{0}” @@ -162,11 +167,6 @@ 使用 “[OSCondition]” 属性 - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf index 3ca2de84f2..e9347165e7 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf @@ -152,6 +152,11 @@ 使用 'DisplayName' 屬性取代字串引數 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' 使用 '{0}' @@ -162,11 +167,6 @@ 使用 '[OSCondition]' 屬性 - - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - Use 'DisplayName' property on '[TestMethod]' instead of '[Description]' attribute - - \ No newline at end of file diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index f5104b78d0..677c1d7cf5 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -33,40 +33,8 @@ public class MyTestClass [TestClass] public class MyTestClass { - [TestMethod(DisplayName = "Description")] - public void MyTestMethod() - { - } - } - """; - - await VerifyCS.VerifyCodeFixAsync(code, fixedCode); - } - - [TestMethod] - public async Task WhenTestMethodHasExistingDisplayNameAndSystemDescriptionAttribute_RemovesDescriptionAttribute() - { - string code = """ - using Microsoft.VisualStudio.TestTools.UnitTesting; - - [TestClass] - public class MyTestClass - { - [TestMethod(DisplayName = "Existing")] - [System.ComponentModel.Description("Description")] - public void [|MyTestMethod|]() - { - } - } - """; - - string fixedCode = """ - using Microsoft.VisualStudio.TestTools.UnitTesting; - - [TestClass] - public class MyTestClass - { - [TestMethod(DisplayName = "Existing")] + [TestMethod] + [Description("Description")] public void MyTestMethod() { } From 2b6086a8bb03cc29308eb10782a77a75c0ef2a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:32:25 +0000 Subject: [PATCH 4/7] Resolve merge conflict and use Simplifier for attribute name qualification Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/6b91d4c0-83e5-407b-a2dc-9fe8712e11dd Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .github/scripts/analyze-duplicates.py | 231 +++ .github/scripts/scan-duplicates.ps1 | 129 ++ .github/workflows/add-tests.lock.yml | 62 +- .github/workflows/add-tests.md | 4 + .../daily-efficiency-improver.lock.yml | 64 +- .../workflows/daily-efficiency-improver.md | 6 +- .github/workflows/daily-file-diet.lock.yml | 28 +- .github/workflows/daily-file-diet.md | 2 + .../daily-malicious-code-scan.lock.yml | 28 +- .../workflows/daily-malicious-code-scan.md | 2 + .../workflows/daily-perf-improver.lock.yml | 64 +- .github/workflows/daily-perf-improver.md | 6 +- .github/workflows/daily-qa.lock.yml | 72 +- .github/workflows/daily-qa.md | 9 +- .../workflows/daily-test-improver.lock.yml | 64 +- .github/workflows/daily-test-improver.md | 6 +- .github/workflows/dedup-analysis.yml | 86 ++ .../workflows/glossary-maintainer.lock.yml | 32 +- .github/workflows/glossary-maintainer.md | 1 + .github/workflows/issue-arborist.lock.yml | 28 +- .github/workflows/issue-arborist.md | 3 +- .github/workflows/lean-proofs.yml | 49 +- .github/workflows/lean-squad.lock.yml | 77 +- .github/workflows/lean-squad.md | 4 + .../workflows/markdown-linter-report.lock.yml | 28 +- .github/workflows/markdown-linter-report.md | 1 + .github/workflows/pr-expert-reviewer.lock.yml | 32 +- .github/workflows/pr-expert-reviewer.md | 4 +- .../workflows/pr-nitpick-reviewer.lock.yml | 39 +- .github/workflows/pr-nitpick-reviewer.md | 19 +- .../pr-test-expert-reviewer.lock.yml | 32 +- .github/workflows/pr-test-expert-reviewer.md | 4 +- .github/workflows/repo-historian.lock.yml | 1284 +++++++++++++++++ .github/workflows/repo-historian.md | 185 +++ .../repository-quality-improver.lock.yml | 28 +- .../workflows/repository-quality-improver.md | 2 + .jscpd.json | 24 + eng/Version.Details.xml | 12 +- eng/Versions.props | 6 +- formal-verification/TARGETS.md | 2 +- .../commandlineparseresult_equals_informal.md | 185 +++ ...nsefilehelper_splitcommandline_informal.md | 225 +++ .../Engine/BFSTestNodeVisitor.cs | 19 +- .../Execution/TestClassInfo.cs | 18 +- .../Execution/TestMethodInfo.cs | 6 +- .../Execution/UnitTestRunner.cs | 18 +- .../Services/TestContextImplementation.cs | 21 +- .../CodeFixResources.resx | 3 + ...DoNotUseSystemDescriptionAttributeFixer.cs | 18 +- .../DuplicateDataRowFixer.cs | 79 + ...Fix.cs => TestMethodShouldBeValidFixer.cs} | 8 +- .../xlf/CodeFixResources.cs.xlf | 5 + .../xlf/CodeFixResources.de.xlf | 5 + .../xlf/CodeFixResources.es.xlf | 5 + .../xlf/CodeFixResources.fr.xlf | 5 + .../xlf/CodeFixResources.it.xlf | 5 + .../xlf/CodeFixResources.ja.xlf | 5 + .../xlf/CodeFixResources.ko.xlf | 5 + .../xlf/CodeFixResources.pl.xlf | 5 + .../xlf/CodeFixResources.pt-BR.xlf | 5 + .../xlf/CodeFixResources.ru.xlf | 5 + .../xlf/CodeFixResources.tr.xlf | 5 + .../xlf/CodeFixResources.zh-Hans.xlf | 5 + .../xlf/CodeFixResources.zh-Hant.xlf | 5 + .../TestContextPropertyUsageAnalyzer.cs | 2 +- .../RetryOrchestrator.cs | 8 +- .../TrxCompareTool.cs | 4 +- .../TrxReportEngine.cs | 4 +- .../Tasks/InvokeTestingPlatformTask.cs | 4 +- .../AbortForMaxFailedTestsExtension.cs | 2 +- .../Helpers/ExitCodes.cs | 34 +- .../NonCooperativeParentProcessListener.cs | 4 +- .../Hosts/CommonTestHost.cs | 8 +- .../Hosts/ConsoleTestHost.cs | 4 +- .../Hosts/ServerTestHost.cs | 4 +- .../Hosts/TestHostBuilder.cs | 2 +- .../Hosts/TestHostControllersTestHost.cs | 8 +- .../Hosts/TestHostOchestratorHost.cs | 2 +- .../Hosts/ToolsTestHost.cs | 8 +- .../IPC/NamedPipeClient.cs | 2 +- .../Messages/PropertyBag.cs | 33 +- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 12 + .../Services/TestApplicationResult.cs | 20 +- .../Assertions/CollectionAssert.Equality.cs | 240 +++ .../CollectionAssert.Equivalence.cs | 342 +++++ .../Assertions/CollectionAssert.Helpers.cs | 286 ++++ .../Assertions/CollectionAssert.Membership.cs | 231 +++ .../Assertions/CollectionAssert.Subset.cs | 127 ++ .../Assertions/CollectionAssert.Type.cs | 84 ++ .../Assertions/CollectionAssert.cs | 1239 +--------------- .../AbortionTests.cs | 2 +- .../AssemblyCleanupTests.cs | 2 +- .../AssemblyResolverTests.cs | 2 +- .../ConfigurationMSTestSettingsTests.cs | 2 +- .../ConfigurationMSTestV2SettingsTests.cs | 2 +- .../ConfigurationSettingsTests.cs | 8 +- .../CustomAttributesTests.cs | 2 +- .../DataSourceTests.cs | 2 +- .../DeploymentItemTests.cs | 2 +- .../DuplicateTestClassAttributeTests.cs | 2 +- .../DynamicDataMethodTests.cs | 2 +- .../FrameworkOnlyTests.cs | 2 +- .../GenericTestMethodTests.cs | 2 +- .../HelpInfoTests.cs | 4 +- .../IgnoreTests.cs | 6 +- .../InconclusiveTests.cs | 4 +- .../LifecycleTests.cs | 2 +- .../MaxFailedTestsExtensionTests.cs | 4 +- .../ParameterizedDataRowTests.cs | 2 +- .../ParameterizedDataSourceTests.cs | 2 +- .../ParameterizedTestTests.cs | 4 +- .../RetryTests.cs | 2 +- .../SdkTests.cs | 6 +- .../ShowOutputOptionTests.cs | 6 +- .../SoftAssertionTests.cs | 14 +- .../TestDiscoveryTests.cs | 4 +- .../TestDiscoveryWarningsTests.cs | 4 +- .../TestFilterTests.cs | 12 +- .../TimeoutCooperativeTestMethodTests.cs | 8 +- .../TimeoutTestMethodTests.cs | 8 +- .../TimeoutTests.cs | 10 +- .../TrxReportTests.cs | 2 +- .../TupleDynamicDataTests.cs | 4 +- .../WinUITests.cs | 2 +- .../AbortionTests.cs | 2 +- .../ConsoleTests.cs | 4 +- .../CrashDumpTests.cs | 8 +- .../CrashPlusHangDumpTests.cs | 4 +- .../CustomBannerTests.cs | 8 +- .../DataConsumerThroughputTests.cs | 2 +- .../DiagnosticTests.cs | 12 +- ...mentVariablesConfigurationProviderTests.cs | 12 +- .../ExecutionRequestCompleteTests.cs | 2 +- .../ExecutionTests.cs | 30 +- .../ForwardCompatibilityTests.cs | 2 +- .../HangDumpOutputTests.cs | 2 +- .../HangDumpProcessTreeTests.cs | 2 +- .../HangDumpTests.cs | 16 +- .../HelpInfoAllExtensionsTests.cs | 6 +- .../HelpInfoTests.cs | 14 +- .../Helpers/AcceptanceAssert.cs | 14 + .../LocalizationFailingTests.cs | 2 +- .../LocalizationTests.cs | 6 +- .../MSBuildTests.GenerateEntryPoint.cs | 4 +- .../MaxFailedTestsExtensionTests.cs | 4 +- .../NoBannerTests.cs | 8 +- .../RetryFailedTestsTests.cs | 18 +- .../TelemetryDisabledTests.cs | 4 +- .../TelemetryTests.cs | 8 +- .../TestHostProcessLifetimeHandlerTests.cs | 2 +- .../TimeoutTests.cs | 14 +- .../TrxDataRowTests.cs | 2 +- .../TrxFailingTestTests.cs | 2 +- .../TrxSkippedTestTests.cs | 4 +- .../TrxTests.cs | 20 +- .../TypeForwardingTests.cs | 2 +- .../UnhandledExceptionPolicyTests.cs | 20 +- ...SystemDescriptionAttributeAnalyzerTests.cs | 68 +- .../DuplicateDataRowAnalyzerTests.cs | 115 +- .../TestMethodShouldBeValidAnalyzerTests.cs | 2 +- .../BFSTestNodeVisitorTests.cs | 33 + .../Execution/TestMethodInfoTests.cs | 39 + .../TestContextImplementationTests.cs | 41 +- .../Helpers/PasteArgumentsTests.cs | 27 + .../Logging/LoggerFactoryProxyTests.cs | 44 + .../Messages/PropertyBagTests.cs | 27 + .../Requests/TreeNodeFilterTests.cs | 14 + .../ServerMode/ServerTests.cs | 2 +- .../Services/TestApplicationResultTests.cs | 38 +- 169 files changed, 5073 insertions(+), 1917 deletions(-) create mode 100644 .github/scripts/analyze-duplicates.py create mode 100644 .github/scripts/scan-duplicates.ps1 create mode 100644 .github/workflows/dedup-analysis.yml create mode 100644 .github/workflows/repo-historian.lock.yml create mode 100644 .github/workflows/repo-historian.md create mode 100644 .jscpd.json create mode 100644 formal-verification/specs/commandlineparseresult_equals_informal.md create mode 100644 formal-verification/specs/responsefilehelper_splitcommandline_informal.md create mode 100644 src/Analyzers/MSTest.Analyzers.CodeFixes/DuplicateDataRowFixer.cs rename src/Analyzers/MSTest.Analyzers.CodeFixes/{TestMethodShouldBeValidCodeFix.cs => TestMethodShouldBeValidFixer.cs} (96%) create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Equality.cs create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Equivalence.cs create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Helpers.cs create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Membership.cs create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Subset.cs create mode 100644 src/TestFramework/TestFramework/Assertions/CollectionAssert.Type.cs create mode 100644 test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/PasteArgumentsTests.cs create mode 100644 test/UnitTests/Microsoft.Testing.Platform.UnitTests/Logging/LoggerFactoryProxyTests.cs diff --git a/.github/scripts/analyze-duplicates.py b/.github/scripts/analyze-duplicates.py new file mode 100644 index 0000000000..c6e055384d --- /dev/null +++ b/.github/scripts/analyze-duplicates.py @@ -0,0 +1,231 @@ +""" +Analyze jscpd duplication report using GitHub Models API. + +Reads the jscpd JSON report, extracts top findings, sends them to an LLM +for classification and refactoring suggestions, and creates/updates a GitHub issue. +""" + +import json +import os +import subprocess +import sys +from pathlib import Path + + +def load_report(report_path: str) -> dict: + with open(report_path) as f: + return json.load(f) + + +def extract_top_findings(report: dict, top_n: int) -> list[dict]: + duplicates = report.get("duplicates", []) + duplicates.sort(key=lambda d: d.get("lines", 0), reverse=True) + return duplicates[:top_n] + + +def read_file_lines(file_path: str, start: int, end: int) -> str: + """Read specific lines from a file. Lines are 1-indexed.""" + try: + path = Path(file_path) + if not path.exists(): + # Try stripping absolute prefix for CI + path = Path(file_path.lstrip("/")) + if not path.exists(): + return f"[Could not read {file_path}]" + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + # Clamp to file bounds + start = max(1, start) + end = min(len(lines), end) + selected = lines[start - 1 : end] + return "\n".join(f"{start + i:4d} | {line}" for i, line in enumerate(selected)) + except Exception as e: + return f"[Error reading {file_path}: {e}]" + + +def build_finding_context(finding: dict) -> str: + """Build a context string for a single duplicate finding.""" + first = finding["firstFile"] + second = finding["secondFile"] + lines = finding.get("lines", 0) + + first_path = first["name"] + first_start = first["startLoc"]["line"] + first_end = first["endLoc"]["line"] + + second_path = second["name"] + second_start = second["startLoc"]["line"] + second_end = second["endLoc"]["line"] + + first_code = read_file_lines(first_path, first_start, first_end) + second_code = read_file_lines(second_path, second_start, second_end) + + return f"""### Duplicate: {lines} lines +**File A**: `{first_path}` (lines {first_start}-{first_end}) +```csharp +{first_code} +``` + +**File B**: `{second_path}` (lines {second_start}-{second_end}) +```csharp +{second_code} +``` +""" + + +def analyze_with_llm(findings_context: str, model: str) -> str: + """Call GitHub Models API to analyze the findings.""" + from openai import OpenAI + + client = OpenAI( + base_url="https://models.inference.ai.azure.com", + api_key=os.environ["GITHUB_TOKEN"], + ) + + system_prompt = """You are a senior .NET developer analyzing code duplication in the MSTest/Microsoft.Testing.Platform repository. + +For each duplicate finding, provide: +1. **Classification**: One of: + - "Extract Method" — identical logic → shared helper + - "Extract Base Class" — duplicated across classes with shared behavior + - "Template Method" — same structure, minor variations → parameterize + - "Intentional" — polyfills, cross-project isolation, or design choice +2. **Priority**: High / Medium / Low based on: + - Lines duplicated (more = higher) + - Whether it's in production code vs infrastructure + - Whether extraction would reduce maintenance burden +3. **Suggested approach**: 1-2 sentences on how to refactor (or why to leave it) +4. **Risk**: Low / Medium / High — considers breaking changes, cross-project dependencies + +IMPORTANT: Files under `src/Polyfills/` are intentionally duplicated across projects (they're compiled into each assembly). Mark those as "Intentional". +Files that are shared helpers duplicated across `MSTest.Analyzers` and `MSTest.SourceGeneration` are strong candidates since those projects could share code via a common project. + +Output a markdown table with columns: Priority, File A, File B, Lines, Classification, Approach, Risk. +Then add a "Summary" section with overall statistics and the top 3 recommended refactorings to tackle first.""" + + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": f"Analyze these duplicate code findings:\n\n{findings_context}", + }, + ], + temperature=0.2, + max_tokens=4096, + ) + + return response.choices[0].message.content + + +def create_or_update_issue(title: str, body: str) -> None: + """Create or update a GitHub issue with the analysis.""" + repo = os.environ.get("GITHUB_REPOSITORY", "") + if not repo: + print("GITHUB_REPOSITORY not set, skipping issue creation") + return + + # Search for existing open issue with our title + result = subprocess.run( + ["gh", "issue", "list", "--repo", repo, "--state", "open", "--search", title, "--json", "number,title"], + capture_output=True, + text=True, + ) + + existing_issues = json.loads(result.stdout) if result.returncode == 0 else [] + matching = [i for i in existing_issues if i["title"] == title] + + if matching: + issue_number = matching[0]["number"] + subprocess.run( + ["gh", "issue", "edit", str(issue_number), "--repo", repo, "--body", body], + check=True, + ) + print(f"Updated existing issue #{issue_number}") + else: + result = subprocess.run( + ["gh", "issue", "create", "--repo", repo, "--title", title, "--body", body, "--label", "tech-debt"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + print(f"Created new issue: {result.stdout.strip()}") + else: + # Label might not exist, retry without it + subprocess.run( + ["gh", "issue", "create", "--repo", repo, "--title", title, "--body", body], + check=True, + ) + + +def main() -> None: + report_path = "artifacts/jscpd/jscpd-report.json" + if not Path(report_path).exists(): + print(f"Report not found at {report_path}") + sys.exit(1) + + top_n = int(os.environ.get("TOP_N", "20")) + model = os.environ.get("MODEL", "openai/gpt-4.1-mini") + + print(f"Loading report from {report_path}") + report = load_report(report_path) + + total = report.get("statistics", {}).get("total", report.get("total", {})) + total_clones = total.get("clones", 0) + total_lines = total.get("duplicatedLines", 0) + percentage = total.get("percentage", 0) + sources = total.get("sources", 0) + + print(f"Found {total_clones} clones across {sources} files ({percentage}% duplication)") + + findings = extract_top_findings(report, top_n) + if not findings: + print("No duplicates found") + sys.exit(0) + + print(f"Analyzing top {len(findings)} findings with {model}...") + + # Build context for each finding + findings_context = "\n\n".join(build_finding_context(f) for f in findings) + + # Call LLM for analysis + analysis = analyze_with_llm(findings_context, model) + + # Build full report + report_md = f"""# Code Duplication Analysis + +> Auto-generated by the [dedup-analysis workflow](../workflows/dedup-analysis.yml) + +## Overview + +| Metric | Value | +|--------|-------| +| Total clones | {total_clones} | +| Duplicated lines | {total_lines} | +| Duplication % | {percentage}% | +| Source files scanned | {sources} | +| Findings analyzed | {len(findings)} | + +## Analysis + +{analysis} + +--- + +Generated by `dedup-analysis` workflow using jscpd + GitHub Models ({model}) +""" + + # Save report + output_path = "artifacts/jscpd/analysis-report.md" + Path(output_path).write_text(report_md, encoding="utf-8") + print(f"Report saved to {output_path}") + + # Create/update GitHub issue + create_or_update_issue( + title="[Tech Debt] Code Duplication Analysis", + body=report_md, + ) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/scan-duplicates.ps1 b/.github/scripts/scan-duplicates.ps1 new file mode 100644 index 0000000000..d3d8d4855b --- /dev/null +++ b/.github/scripts/scan-duplicates.ps1 @@ -0,0 +1,129 @@ +<# +.SYNOPSIS + Scans the testfx source code for duplicated code blocks using jscpd. + +.DESCRIPTION + Runs jscpd on the src/ directory and produces a JSON report in artifacts/jscpd/. + Optionally filters results by a minimum number of duplicated lines or a specific + subdirectory. + +.PARAMETER Path + The subdirectory under src/ to scan. Defaults to scanning all of src/. + +.PARAMETER MinLines + Minimum number of duplicated lines to report. Defaults to 6. + +.PARAMETER MinTokens + Minimum number of duplicated tokens to report. Defaults to 50. + +.PARAMETER OutputDir + Directory for the JSON report. Defaults to artifacts/jscpd. + +.PARAMETER TopN + Show only the top N results sorted by duplication size. Defaults to 0 (all). + +.EXAMPLE + .\.github\scripts\scan-duplicates.ps1 + .\.github\scripts\scan-duplicates.ps1 -Path "src/Platform/Microsoft.Testing.Platform" -MinLines 10 + .\.github\scripts\scan-duplicates.ps1 -TopN 20 +#> +param( + [string]$Path = "src", + [int]$MinLines = 6, + [int]$MinTokens = 50, + [string]$OutputDir = "artifacts/jscpd", + [int]$TopN = 0 +) + +$ErrorActionPreference = "Stop" +$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)) +Push-Location $repoRoot + +try { + # Ensure output directory exists + if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null + } + + Write-Host "=== Scanning for duplicated code ===" -ForegroundColor Cyan + Write-Host " Path: $Path" -ForegroundColor Gray + Write-Host " MinLines: $MinLines" -ForegroundColor Gray + Write-Host " MinTokens: $MinTokens" -ForegroundColor Gray + Write-Host " Output: $OutputDir" -ForegroundColor Gray + Write-Host "" + + # Run jscpd (threshold=100 to avoid failing on duplication percentage) + $jscpdArgs = @( + "jscpd" + $Path + "--min-lines", $MinLines + "--min-tokens", $MinTokens + "--reporters", "json,consoleFull" + "--output", $OutputDir + "--format", "csharp" + "--threshold", "100" + "--ignore", "**/bin/**,**/obj/**,**/artifacts/**,**/*.Designer.cs,**/*.g.cs,**/*.xlf,**/*.resx,**/PublicAPI.*.txt,**/test/**,**/samples/**,**/formal-verification/**" + ) + + npx @jscpdArgs + $exitCode = $LASTEXITCODE + # jscpd exits 1 when over threshold — not an error for our purposes + if ($exitCode -ne 0) { + Write-Host " (jscpd exited with code $exitCode)" -ForegroundColor DarkGray + } + + # Parse and summarize results + $reportPath = Join-Path $OutputDir "jscpd-report.json" + if (Test-Path $reportPath) { + $report = Get-Content $reportPath -Raw | ConvertFrom-Json + + $duplicates = $report.duplicates + if ($TopN -gt 0 -and $duplicates.Count -gt $TopN) { + $duplicates = $duplicates | + Sort-Object { $_.lines } -Descending | + Select-Object -First $TopN + } + + Write-Host "" + Write-Host "=== Summary ===" -ForegroundColor Cyan + Write-Host " Total clones found: $($report.duplicates.Count)" -ForegroundColor Yellow + Write-Host " Files with duplicates: $($report.statistics.total.sources)" -ForegroundColor Yellow + + if ($report.statistics.total.PSObject.Properties.Name -contains "percentage") { + Write-Host " Duplication percentage: $($report.statistics.total.percentage)%" -ForegroundColor Yellow + } + + Write-Host "" + Write-Host " Report saved to: $reportPath" -ForegroundColor Green + Write-Host "" + + # Print top findings + if ($duplicates.Count -gt 0) { + Write-Host "=== Top Findings ===" -ForegroundColor Cyan + $rank = 1 + foreach ($dup in $duplicates) { + $firstFile = $dup.firstFile.name + $secondFile = $dup.secondFile.name + $firstStart = $dup.firstFile.startLoc.line + $firstEnd = $dup.firstFile.endLoc.line + $secondStart = $dup.secondFile.startLoc.line + $secondEnd = $dup.secondFile.endLoc.line + $lines = $dup.lines + + Write-Host " [$rank] $lines lines duplicated:" -ForegroundColor White + Write-Host " A: $firstFile (lines $firstStart-$firstEnd)" -ForegroundColor Gray + Write-Host " B: $secondFile (lines $secondStart-$secondEnd)" -ForegroundColor Gray + Write-Host "" + $rank++ + + if ($TopN -gt 0 -and $rank -gt $TopN) { break } + } + } + } + else { + Write-Host "No report generated. jscpd may not have found any duplicates." -ForegroundColor Yellow + } +} +finally { + Pop-Location +} diff --git a/.github/workflows/add-tests.lock.yml b/.github/workflows/add-tests.lock.yml index f319763cea..6c55bcf871 100644 --- a/.github/workflows/add-tests.lock.yml +++ b/.github/workflows/add-tests.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f1ae58097d136e1aedc8b8b3811bab47f53151661049a87c330f93da94101338","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5f8f2e33ea84b9c6a994e3da0ccbb59691f3b79e86d4ecfd690a0f2356e7c386","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -115,6 +115,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -210,19 +213,19 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_dec99d8f641d87ac_EOF' + cat << 'GH_AW_PROMPT_f53be347eff01c81_EOF' - GH_AW_PROMPT_dec99d8f641d87ac_EOF + GH_AW_PROMPT_f53be347eff01c81_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_dec99d8f641d87ac_EOF' + cat << 'GH_AW_PROMPT_f53be347eff01c81_EOF' Tools: add_comment(max:3), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_dec99d8f641d87ac_EOF + GH_AW_PROMPT_f53be347eff01c81_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_dec99d8f641d87ac_EOF' + cat << 'GH_AW_PROMPT_f53be347eff01c81_EOF' The following GitHub context information is available for this workflow: @@ -252,16 +255,16 @@ jobs: {{/if}} - GH_AW_PROMPT_dec99d8f641d87ac_EOF + GH_AW_PROMPT_f53be347eff01c81_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_dec99d8f641d87ac_EOF' + cat << 'GH_AW_PROMPT_f53be347eff01c81_EOF' {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/add-tests.md}} - GH_AW_PROMPT_dec99d8f641d87ac_EOF + GH_AW_PROMPT_f53be347eff01c81_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -427,16 +430,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -444,9 +444,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1a590cca58967b10_EOF' - {"add_comment":{"max":3},"create_pull_request":{"draft":true,"labels":["test","automated"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[tests] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_1a590cca58967b10_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_d46391cf00801004_EOF' + {"add_comment":{"max":3},"create_pull_request":{"draft":true,"labels":["test","automated"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[tests] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_d46391cf00801004_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -645,8 +645,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -667,7 +665,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_4144be0fedbe3eb1_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_d3b80a76769b00d8_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -675,14 +673,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "pull_requests,repos" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -708,7 +710,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_4144be0fedbe3eb1_EOF + GH_AW_MCP_CONFIG_d3b80a76769b00d8_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -912,6 +914,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -978,7 +982,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Add Tests for PR Changes" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1376,7 +1380,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"test\",\"automated\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[tests] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"test\",\"automated\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[tests] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/add-tests.md b/.github/workflows/add-tests.md index 8490056ca1..f9501794c1 100644 --- a/.github/workflows/add-tests.md +++ b/.github/workflows/add-tests.md @@ -15,11 +15,15 @@ imports: tools: github: + lockdown: true toolsets: [pull_requests, repos] + min-integrity: none edit: bash: ["dotnet", "git", "find", "ls", "cat", "grep", "head", "tail", "wc", "mkdir"] safe-outputs: + noop: + report-as-issue: false create-pull-request: title-prefix: "[tests] " labels: [test, automated] diff --git a/.github/workflows/daily-efficiency-improver.lock.yml b/.github/workflows/daily-efficiency-improver.lock.yml index 85dddb2d38..0955f58e95 100644 --- a/.github/workflows/daily-efficiency-improver.lock.yml +++ b/.github/workflows/daily-efficiency-improver.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"a05b9517ad94d4d889af3a3c29ba12fd875e72300a7831510c6f33633dc622c8","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f2a5b339ab7a0a26a8af8e14cda30b35ee27808521478197e7e3bdfd200d38ff","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -114,6 +114,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -188,21 +191,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_d025362b5185cea9_EOF' + cat << 'GH_AW_PROMPT_83c5eb67f7584cea_EOF' - GH_AW_PROMPT_d025362b5185cea9_EOF + GH_AW_PROMPT_83c5eb67f7584cea_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_d025362b5185cea9_EOF' + cat << 'GH_AW_PROMPT_83c5eb67f7584cea_EOF' Tools: add_comment(max:3), create_issue(max:4), update_issue, create_pull_request, push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_d025362b5185cea9_EOF + GH_AW_PROMPT_83c5eb67f7584cea_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_d025362b5185cea9_EOF' + cat << 'GH_AW_PROMPT_83c5eb67f7584cea_EOF' The following GitHub context information is available for this workflow: @@ -232,13 +235,13 @@ jobs: {{/if}} - GH_AW_PROMPT_d025362b5185cea9_EOF + GH_AW_PROMPT_83c5eb67f7584cea_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_d025362b5185cea9_EOF' + cat << 'GH_AW_PROMPT_83c5eb67f7584cea_EOF' {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/daily-efficiency-improver.md}} - GH_AW_PROMPT_d025362b5185cea9_EOF + GH_AW_PROMPT_83c5eb67f7584cea_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -427,16 +430,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -444,9 +444,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_00cc6d7be86f5e15_EOF' - {"add_comment":{"hide_older_comments":true,"max":3,"target":"*"},"create_issue":{"labels":["automation","efficiency","green-software"],"max":4,"title_prefix":"[Efficiency Improver] "},"create_pull_request":{"draft":true,"labels":["automation","efficiency","green-software"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Efficiency Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Efficiency Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_00cc6d7be86f5e15_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e951313a485e6b88_EOF' + {"add_comment":{"hide_older_comments":true,"max":3,"target":"*"},"create_issue":{"labels":["automation","efficiency","green-software"],"max":4,"title_prefix":"[Efficiency Improver] "},"create_pull_request":{"draft":true,"labels":["automation","efficiency","green-software"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Efficiency Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Efficiency Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} + GH_AW_SAFE_OUTPUTS_CONFIG_e951313a485e6b88_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -755,8 +755,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -777,7 +775,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_eec94aca39be4144_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_a604f7e430647ee0_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -785,14 +783,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" + "GITHUB_TOOLSETS": "repos,pull_requests,issues,discussions" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -818,7 +820,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_eec94aca39be4144_EOF + GH_AW_MCP_CONFIG_a604f7e430647ee0_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1003,6 +1005,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -1070,7 +1074,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Daily Efficiency Improver" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1492,7 +1496,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"efficiency\",\"green-software\"],\"max\":4,\"title_prefix\":\"[Efficiency Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"efficiency\",\"green-software\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Efficiency Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Efficiency Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"efficiency\",\"green-software\"],\"max\":4,\"title_prefix\":\"[Efficiency Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"efficiency\",\"green-software\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Efficiency Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Efficiency Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\"}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-efficiency-improver.md b/.github/workflows/daily-efficiency-improver.md index aab40b1dab..1d7ab9e410 100644 --- a/.github/workflows/daily-efficiency-improver.md +++ b/.github/workflows/daily-efficiency-improver.md @@ -49,11 +49,15 @@ safe-outputs: update-issue: target: "*" max: 1 + noop: + report-as-issue: false tools: web-fetch: github: - toolsets: [all] + lockdown: true + toolsets: [repos, pull_requests, issues, discussions] + min-integrity: none bash: true repo-memory: true --- diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index ff6d0aa072..d336d5f7cc 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7011574ecd5c2192a5afe83377b978ea7282e5defe2909cdce2248483985113d","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"8984d225ddee5d75dd45eefe6aeb2c51a1dc00a03513fca540778d9a7365bf10","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -169,14 +169,14 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_56e596bf604e3c90_EOF' + cat << 'GH_AW_PROMPT_8190431257cf04b9_EOF' - GH_AW_PROMPT_56e596bf604e3c90_EOF + GH_AW_PROMPT_8190431257cf04b9_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_56e596bf604e3c90_EOF' + cat << 'GH_AW_PROMPT_8190431257cf04b9_EOF' Tools: create_issue, missing_tool, missing_data, noop @@ -208,12 +208,12 @@ jobs: {{/if}} - GH_AW_PROMPT_56e596bf604e3c90_EOF + GH_AW_PROMPT_8190431257cf04b9_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_56e596bf604e3c90_EOF' + cat << 'GH_AW_PROMPT_8190431257cf04b9_EOF' {{#runtime-import .github/workflows/daily-file-diet.md}} - GH_AW_PROMPT_56e596bf604e3c90_EOF + GH_AW_PROMPT_8190431257cf04b9_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -388,9 +388,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ae930b1efbd90971_EOF' - {"create_issue":{"assignees":["copilot"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_ae930b1efbd90971_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_00a925a96674df54_EOF' + {"create_issue":{"assignees":["copilot"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_00a925a96674df54_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -585,7 +585,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_c9743012f0ab3200_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_2ac9f9416a622960_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -626,7 +626,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_c9743012f0ab3200_EOF + GH_AW_MCP_CONFIG_2ac9f9416a622960_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -884,7 +884,7 @@ jobs: GH_AW_TRACKER_ID: "daily-file-diet" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1239,7 +1239,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_ASSIGN_COPILOT: "true" GH_AW_ASSIGN_TO_AGENT_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/daily-file-diet.md b/.github/workflows/daily-file-diet.md index 2538096f2b..c8b251675c 100644 --- a/.github/workflows/daily-file-diet.md +++ b/.github/workflows/daily-file-diet.md @@ -15,6 +15,8 @@ permissions: tracker-id: daily-file-diet safe-outputs: + noop: + report-as-issue: false create-issue: expires: 2d title-prefix: "[file-diet] " diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 2342e6cdd9..21c2a7b2b0 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"85ef044149014c98cecff5dd2f74ddb15677ed6c42bfb6ba92dee1487629e657","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"53afd9a89de1e94c88b5b58c9a04a1a974c2ed714837232ff917c88b2dec3425","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/codeql-action/upload-sarif","sha":"0e9f55954318745b37b7933c693bc093f7336125","version":"v4.35.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -165,14 +165,14 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8eb46106df20a264_EOF' + cat << 'GH_AW_PROMPT_83e111146cc3b7c7_EOF' - GH_AW_PROMPT_8eb46106df20a264_EOF + GH_AW_PROMPT_83e111146cc3b7c7_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_8eb46106df20a264_EOF' + cat << 'GH_AW_PROMPT_83e111146cc3b7c7_EOF' Tools: create_code_scanning_alert, missing_tool, missing_data, noop @@ -204,12 +204,12 @@ jobs: {{/if}} - GH_AW_PROMPT_8eb46106df20a264_EOF + GH_AW_PROMPT_83e111146cc3b7c7_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8eb46106df20a264_EOF' + cat << 'GH_AW_PROMPT_83e111146cc3b7c7_EOF' {{#runtime-import .github/workflows/daily-malicious-code-scan.md}} - GH_AW_PROMPT_8eb46106df20a264_EOF + GH_AW_PROMPT_83e111146cc3b7c7_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -381,9 +381,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_64e2b6236ebe698b_EOF' - {"create_code_scanning_alert":{"driver":"Malicious Code Scanner"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_64e2b6236ebe698b_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1c8e1402531a7203_EOF' + {"create_code_scanning_alert":{"driver":"Malicious Code Scanner"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_1c8e1402531a7203_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -584,7 +584,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_43589d73409f871f_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_584855d2df07be36_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -625,7 +625,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_43589d73409f871f_EOF + GH_AW_MCP_CONFIG_584855d2df07be36_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -882,7 +882,7 @@ jobs: GH_AW_TRACKER_ID: "malicious-code-scan" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1016,7 +1016,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_code_scanning_alert\":{\"driver\":\"Malicious Code Scanner\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_code_scanning_alert\":{\"driver\":\"Malicious Code Scanner\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/daily-malicious-code-scan.md b/.github/workflows/daily-malicious-code-scan.md index 9c7df58f44..69c7abf0e3 100644 --- a/.github/workflows/daily-malicious-code-scan.md +++ b/.github/workflows/daily-malicious-code-scan.md @@ -18,6 +18,8 @@ tools: bash: [git, grep, sort, uniq, cat, tr, head, date, file] safe-outputs: + noop: + report-as-issue: false create-code-scanning-alert: driver: "Malicious Code Scanner" threat-detection: false diff --git a/.github/workflows/daily-perf-improver.lock.yml b/.github/workflows/daily-perf-improver.lock.yml index 834f442180..b65c0dea44 100644 --- a/.github/workflows/daily-perf-improver.lock.yml +++ b/.github/workflows/daily-perf-improver.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"395af994e8a27440026688530770bb1f79499fbc80c14551b2541d0727d20074","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"4ae2b04463fb7872ce8ebe14cad724bef391877c6cdafbaa6ba5e3f9ae40d09d","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -132,6 +132,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -229,21 +232,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ed68094b538afe37_EOF' + cat << 'GH_AW_PROMPT_a489f6bef6740bde_EOF' - GH_AW_PROMPT_ed68094b538afe37_EOF + GH_AW_PROMPT_a489f6bef6740bde_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_ed68094b538afe37_EOF' + cat << 'GH_AW_PROMPT_a489f6bef6740bde_EOF' Tools: add_comment(max:3), create_issue(max:4), update_issue, create_pull_request(max:4), push_to_pull_request_branch(max:4), missing_tool, missing_data, noop - GH_AW_PROMPT_ed68094b538afe37_EOF + GH_AW_PROMPT_a489f6bef6740bde_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_ed68094b538afe37_EOF' + cat << 'GH_AW_PROMPT_a489f6bef6740bde_EOF' The following GitHub context information is available for this workflow: @@ -273,7 +276,7 @@ jobs: {{/if}} - GH_AW_PROMPT_ed68094b538afe37_EOF + GH_AW_PROMPT_a489f6bef6740bde_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" @@ -281,11 +284,11 @@ jobs: if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" fi - cat << 'GH_AW_PROMPT_ed68094b538afe37_EOF' + cat << 'GH_AW_PROMPT_a489f6bef6740bde_EOF' {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/daily-perf-improver.md}} - GH_AW_PROMPT_ed68094b538afe37_EOF + GH_AW_PROMPT_a489f6bef6740bde_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -481,16 +484,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -498,9 +498,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2ad7ceccb3d0e3cb_EOF' - {"add_comment":{"hide_older_comments":true,"max":3,"target":"*"},"create_issue":{"labels":["automation","performance"],"max":4,"title_prefix":"[Perf Improver] "},"create_pull_request":{"draft":true,"labels":["automation","performance"],"max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Perf Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Perf Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Perf Improver] "}} - GH_AW_SAFE_OUTPUTS_CONFIG_2ad7ceccb3d0e3cb_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2814673ebbdba5c9_EOF' + {"add_comment":{"hide_older_comments":true,"max":3,"target":"*"},"create_issue":{"labels":["automation","performance"],"max":4,"title_prefix":"[Perf Improver] "},"create_pull_request":{"draft":true,"labels":["automation","performance"],"max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Perf Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Perf Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Perf Improver] "}} + GH_AW_SAFE_OUTPUTS_CONFIG_2814673ebbdba5c9_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -809,8 +809,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -831,7 +829,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_fe160076fcd335f7_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_3dc4f4f2fa279d74_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -839,14 +837,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" + "GITHUB_TOOLSETS": "repos,pull_requests,issues,discussions" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -872,7 +874,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fe160076fcd335f7_EOF + GH_AW_MCP_CONFIG_3dc4f4f2fa279d74_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1058,6 +1060,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -1125,7 +1129,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Daily Perf Improver" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1604,7 +1608,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"performance\"],\"max\":4,\"title_prefix\":\"[Perf Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"performance\"],\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Perf Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Perf Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Perf Improver] \"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"performance\"],\"max\":4,\"title_prefix\":\"[Perf Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"performance\"],\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Perf Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Perf Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Perf Improver] \"}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-perf-improver.md b/.github/workflows/daily-perf-improver.md index f9c6857360..b0aa5b1348 100644 --- a/.github/workflows/daily-perf-improver.md +++ b/.github/workflows/daily-perf-improver.md @@ -59,11 +59,15 @@ safe-outputs: target: "*" title-prefix: "[Perf Improver] " max: 1 + noop: + report-as-issue: false tools: web-fetch: github: - toolsets: [all] + lockdown: true + toolsets: [repos, pull_requests, issues, discussions] + min-integrity: none bash: true repo-memory: true --- diff --git a/.github/workflows/daily-qa.lock.yml b/.github/workflows/daily-qa.lock.yml index 7be823df3d..449882149d 100644 --- a/.github/workflows/daily-qa.lock.yml +++ b/.github/workflows/daily-qa.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"265781662005608c554633898e261f68421dd5f8d51f56d12462242210bdf836","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"2a61682f04e4ef95c960428cc02b68486f1fc0ba0a46d3cb09973f12ee68e562","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -107,12 +107,15 @@ jobs: GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dc.services.visualstudio.com","pkgs.dev.azure.com"]' + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.25.20" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -173,19 +176,19 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_146bc0fc2e61a0ea_EOF' + cat << 'GH_AW_PROMPT_f1605c4b67c39936_EOF' - GH_AW_PROMPT_146bc0fc2e61a0ea_EOF + GH_AW_PROMPT_f1605c4b67c39936_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_146bc0fc2e61a0ea_EOF' + cat << 'GH_AW_PROMPT_f1605c4b67c39936_EOF' Tools: add_comment(max:5), create_discussion, create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_146bc0fc2e61a0ea_EOF + GH_AW_PROMPT_f1605c4b67c39936_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_146bc0fc2e61a0ea_EOF' + cat << 'GH_AW_PROMPT_f1605c4b67c39936_EOF' The following GitHub context information is available for this workflow: @@ -215,13 +218,13 @@ jobs: {{/if}} - GH_AW_PROMPT_146bc0fc2e61a0ea_EOF + GH_AW_PROMPT_f1605c4b67c39936_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_146bc0fc2e61a0ea_EOF' + cat << 'GH_AW_PROMPT_f1605c4b67c39936_EOF' {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/daily-qa.md}} - GH_AW_PROMPT_146bc0fc2e61a0ea_EOF + GH_AW_PROMPT_f1605c4b67c39936_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -381,16 +384,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -400,9 +400,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_197072974b3033b7_EOF - {"add_comment":{"max":5,"target":"*"},"create_discussion":{"category":"q-a","expires":168,"fallback_to_issue":true,"max":1,"title_prefix":"${GITHUB_WORKFLOW}"},"create_pull_request":{"draft":true,"labels":["automation","qa"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"]},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_197072974b3033b7_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_61541816ded5d39d_EOF + {"add_comment":{"max":5,"target":"*"},"create_discussion":{"category":"q-a","expires":168,"fallback_to_issue":true,"max":1,"title_prefix":"${GITHUB_WORKFLOW}"},"create_pull_request":{"draft":true,"labels":["automation","qa"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"]},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_61541816ded5d39d_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -628,8 +628,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -650,7 +648,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_fa95ff50f0e24da9_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_8499fbe66757a7d8_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -658,14 +656,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" + "GITHUB_TOOLSETS": "repos,pull_requests,issues,discussions" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -691,7 +693,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fa95ff50f0e24da9_EOF + GH_AW_MCP_CONFIG_8499fbe66757a7d8_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -709,7 +711,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE @@ -795,7 +797,7 @@ jobs: uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GH_AW_ALLOWED_GITHUB_REFS: "" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} @@ -868,6 +870,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -934,7 +938,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Daily QA" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1274,10 +1278,10 @@ jobs: uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_discussion\":{\"category\":\"q-a\",\"expires\":168,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"${{ github.workflow }}\"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"qa\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_discussion\":{\"category\":\"q-a\",\"expires\":168,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"${{ github.workflow }}\"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"qa\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-qa.md b/.github/workflows/daily-qa.md index 67d13d8757..accb840a38 100644 --- a/.github/workflows/daily-qa.md +++ b/.github/workflows/daily-qa.md @@ -16,13 +16,14 @@ permissions: read-all network: allowed: - defaults - - "dc.services.visualstudio.com" - - "pkgs.dev.azure.com" + - "dotnet" imports: - shared/repo-build-setup.md safe-outputs: + noop: + report-as-issue: false mentions: false allowed-github-references: [] create-discussion: @@ -38,7 +39,9 @@ safe-outputs: tools: github: - toolsets: [all] + lockdown: true + toolsets: [repos, pull_requests, issues, discussions] + min-integrity: none web-fetch: bash: true --- diff --git a/.github/workflows/daily-test-improver.lock.yml b/.github/workflows/daily-test-improver.lock.yml index 8621e0b96f..803ae7eacf 100644 --- a/.github/workflows/daily-test-improver.lock.yml +++ b/.github/workflows/daily-test-improver.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1cff218e23a90ef30c44904dc9a70ae17b9b625d66a98dceec0eec495e64fc64","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"aea2f1797257f7511afb1077c19e5977cc4bc8346ee956662fb66c04253fd1a0","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -154,6 +154,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -251,21 +254,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_6b8a3a437bcaa112_EOF' + cat << 'GH_AW_PROMPT_6b906d02e98a388e_EOF' - GH_AW_PROMPT_6b8a3a437bcaa112_EOF + GH_AW_PROMPT_6b906d02e98a388e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_6b8a3a437bcaa112_EOF' + cat << 'GH_AW_PROMPT_6b906d02e98a388e_EOF' Tools: add_comment(max:10), create_issue(max:4), update_issue, create_pull_request(max:4), push_to_pull_request_branch(max:4), missing_tool, missing_data, noop - GH_AW_PROMPT_6b8a3a437bcaa112_EOF + GH_AW_PROMPT_6b906d02e98a388e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_6b8a3a437bcaa112_EOF' + cat << 'GH_AW_PROMPT_6b906d02e98a388e_EOF' The following GitHub context information is available for this workflow: @@ -295,7 +298,7 @@ jobs: {{/if}} - GH_AW_PROMPT_6b8a3a437bcaa112_EOF + GH_AW_PROMPT_6b906d02e98a388e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" @@ -303,11 +306,11 @@ jobs: if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" fi - cat << 'GH_AW_PROMPT_6b8a3a437bcaa112_EOF' + cat << 'GH_AW_PROMPT_6b906d02e98a388e_EOF' {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/daily-test-improver.md}} - GH_AW_PROMPT_6b8a3a437bcaa112_EOF + GH_AW_PROMPT_6b906d02e98a388e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -497,16 +500,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -514,9 +514,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_eacdf709854443b0_EOF' - {"add_comment":{"hide_older_comments":true,"max":10,"target":"*"},"create_issue":{"labels":["automation","testing"],"max":4,"title_prefix":"[Test Improver] "},"create_pull_request":{"draft":true,"labels":["automation","testing"],"max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Test Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Test Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Test Improver] "}} - GH_AW_SAFE_OUTPUTS_CONFIG_eacdf709854443b0_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7198034c40fda82a_EOF' + {"add_comment":{"hide_older_comments":true,"max":10,"target":"*"},"create_issue":{"labels":["automation","testing"],"max":4,"title_prefix":"[Test Improver] "},"create_pull_request":{"draft":true,"labels":["automation","testing"],"max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Test Improver] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Test Improver] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Test Improver] "}} + GH_AW_SAFE_OUTPUTS_CONFIG_7198034c40fda82a_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -825,8 +825,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -847,7 +845,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_c69660043de5c3d2_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_3063cdace8fce2d6_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -855,14 +853,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" + "GITHUB_TOOLSETS": "repos,pull_requests,issues,discussions" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -888,7 +890,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_c69660043de5c3d2_EOF + GH_AW_MCP_CONFIG_3063cdace8fce2d6_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1074,6 +1076,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -1141,7 +1145,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Daily Test Improver" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1620,7 +1624,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":10,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"testing\"],\"max\":4,\"title_prefix\":\"[Test Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"testing\"],\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Test Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Test Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Test Improver] \"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":10,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"testing\"],\"max\":4,\"title_prefix\":\"[Test Improver] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"testing\"],\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Test Improver] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Test Improver] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Test Improver] \"}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-test-improver.md b/.github/workflows/daily-test-improver.md index 92d05312dc..f7abc44996 100644 --- a/.github/workflows/daily-test-improver.md +++ b/.github/workflows/daily-test-improver.md @@ -52,12 +52,16 @@ safe-outputs: target: "*" title-prefix: "[Test Improver] " max: 1 + noop: + report-as-issue: false tools: web-fetch: bash: true github: - toolsets: [all] + lockdown: true + toolsets: [repos, pull_requests, issues, discussions] + min-integrity: none repo-memory: true --- diff --git a/.github/workflows/dedup-analysis.yml b/.github/workflows/dedup-analysis.yml new file mode 100644 index 0000000000..534192339a --- /dev/null +++ b/.github/workflows/dedup-analysis.yml @@ -0,0 +1,86 @@ +name: Code Duplication Analysis + +on: + workflow_dispatch: + inputs: + path: + description: 'Source path to scan (relative to repo root)' + required: false + default: 'src' + min_lines: + description: 'Minimum duplicated lines to report' + required: false + default: '6' + min_tokens: + description: 'Minimum duplicated tokens to report' + required: false + default: '50' + top_n: + description: 'Number of top findings to analyze with LLM' + required: false + default: '20' + model: + description: 'GitHub Models model to use for analysis' + required: false + default: 'openai/gpt-4.1-mini' + schedule: + # Run weekly on Monday at 8:00 UTC + - cron: '0 8 * * 1' + +permissions: + contents: read + issues: write + models: read + +jobs: + scan-and-analyze: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + npm install -g jscpd + pip install openai + + - name: Run jscpd scan + run: | + mkdir -p artifacts/jscpd + jscpd ${{ inputs.path || 'src' }} \ + --min-lines ${{ inputs.min_lines || '6' }} \ + --min-tokens ${{ inputs.min_tokens || '50' }} \ + --reporters json \ + --output artifacts/jscpd \ + --format csharp \ + --threshold 100 \ + --ignore "**/bin/**,**/obj/**,**/artifacts/**,**/*.Designer.cs,**/*.g.cs,**/*.xlf,**/*.resx,**/PublicAPI.*.txt,**/test/**,**/samples/**,**/formal-verification/**" + + - name: Analyze duplicates with GitHub Models + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TOP_N: ${{ inputs.top_n || '20' }} + MODEL: ${{ inputs.model || 'openai/gpt-4.1-mini' }} + run: python .github/scripts/analyze-duplicates.py + + - name: Upload jscpd report + uses: actions/upload-artifact@v4 + with: + name: jscpd-report + path: artifacts/jscpd/ + + - name: Upload analysis report + uses: actions/upload-artifact@v4 + with: + name: duplication-analysis + path: artifacts/jscpd/analysis-report.md diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 73eb76ee60..41ac818c84 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"fe8a3fc3b0f88f639a68aeacbf5592fbbfde0d063538763c3bcabbf65cb33841","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"8c29e2cdb54953111ddd0b677d8ea2e7df9e1a793c83561c06ff686152430d93","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -167,20 +167,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_3789b8c359ae542c_EOF' + cat << 'GH_AW_PROMPT_0ad780ea9b1a2c29_EOF' - GH_AW_PROMPT_3789b8c359ae542c_EOF + GH_AW_PROMPT_0ad780ea9b1a2c29_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_3789b8c359ae542c_EOF' + cat << 'GH_AW_PROMPT_0ad780ea9b1a2c29_EOF' Tools: create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_3789b8c359ae542c_EOF + GH_AW_PROMPT_0ad780ea9b1a2c29_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_3789b8c359ae542c_EOF' + cat << 'GH_AW_PROMPT_0ad780ea9b1a2c29_EOF' The following GitHub context information is available for this workflow: @@ -210,12 +210,12 @@ jobs: {{/if}} - GH_AW_PROMPT_3789b8c359ae542c_EOF + GH_AW_PROMPT_0ad780ea9b1a2c29_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_3789b8c359ae542c_EOF' + cat << 'GH_AW_PROMPT_0ad780ea9b1a2c29_EOF' {{#runtime-import .github/workflows/glossary-maintainer.md}} - GH_AW_PROMPT_3789b8c359ae542c_EOF + GH_AW_PROMPT_0ad780ea9b1a2c29_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -408,9 +408,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8dca62cf4c666c76_EOF' - {"create_pull_request":{"draft":false,"expires":48,"labels":["documentation","glossary"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[docs] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_8dca62cf4c666c76_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a7541e9977ce7a2f_EOF' + {"create_pull_request":{"draft":false,"expires":48,"labels":["documentation","glossary"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[docs] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_a7541e9977ce7a2f_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -608,7 +608,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_3742cc4dd52acbee_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_7a16c0bcf340ed36_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -649,7 +649,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_3742cc4dd52acbee_EOF + GH_AW_MCP_CONFIG_7a16c0bcf340ed36_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -902,7 +902,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Glossary Maintainer" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1240,7 +1240,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"expires\":48,\"labels\":[\"documentation\",\"glossary\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[docs] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"expires\":48,\"labels\":[\"documentation\",\"glossary\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[docs] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/glossary-maintainer.md b/.github/workflows/glossary-maintainer.md index 9c847f0e9b..ef5c0f316f 100644 --- a/.github/workflows/glossary-maintainer.md +++ b/.github/workflows/glossary-maintainer.md @@ -26,6 +26,7 @@ safe-outputs: draft: false protected-files: fallback-to-issue noop: + report-as-issue: false tools: cache-memory: true diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 7107262922..ce506fc30e 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"9fdf523ad5f4160cc5fea8dbb6cb3ccc624824620835f4be74ceff2e5a4356a8","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3f6b2d24d450c19b62a379412443e6b91615328347c9f84a5e0d71219cf9cc5a","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -167,14 +167,14 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_4cd756b0532ebe9c_EOF' + cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' - GH_AW_PROMPT_4cd756b0532ebe9c_EOF + GH_AW_PROMPT_ace4b671ae98210e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_4cd756b0532ebe9c_EOF' + cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' Tools: create_issue(max:5), link_sub_issue(max:50), missing_tool, missing_data, noop @@ -206,12 +206,12 @@ jobs: {{/if}} - GH_AW_PROMPT_4cd756b0532ebe9c_EOF + GH_AW_PROMPT_ace4b671ae98210e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_4cd756b0532ebe9c_EOF' + cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' {{#runtime-import .github/workflows/issue-arborist.md}} - GH_AW_PROMPT_4cd756b0532ebe9c_EOF + GH_AW_PROMPT_ace4b671ae98210e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -400,9 +400,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_42504f58270eb06f_EOF' - {"create_issue":{"expires":48,"group":true,"max":5,"title_prefix":"[Parent] "},"create_report_incomplete_issue":{},"link_sub_issue":{"max":50},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_42504f58270eb06f_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7bb66e2f03e7c085_EOF' + {"create_issue":{"expires":48,"group":true,"max":5,"title_prefix":"[Parent] "},"create_report_incomplete_issue":{},"link_sub_issue":{"max":50},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_7bb66e2f03e7c085_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -614,7 +614,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_17d2269b8b9e7b39_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_7d7f1b933958e599_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -659,7 +659,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_17d2269b8b9e7b39_EOF + GH_AW_MCP_CONFIG_7d7f1b933958e599_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -918,7 +918,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Issue Arborist" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1225,7 +1225,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"group\":true,\"max\":5,\"title_prefix\":\"[Parent] \"},\"create_report_incomplete_issue\":{},\"link_sub_issue\":{\"max\":50},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"group\":true,\"max\":5,\"title_prefix\":\"[Parent] \"},\"create_report_incomplete_issue\":{},\"link_sub_issue\":{\"max\":50},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-arborist.md b/.github/workflows/issue-arborist.md index 53efdb1633..40d4f183db 100644 --- a/.github/workflows/issue-arborist.md +++ b/.github/workflows/issue-arborist.md @@ -55,7 +55,8 @@ safe-outputs: group: true link-sub-issue: max: 50 - noop: {} + noop: + report-as-issue: false timeout-minutes: 15 --- diff --git a/.github/workflows/lean-proofs.yml b/.github/workflows/lean-proofs.yml index d815ccb312..b35a7274fa 100644 --- a/.github/workflows/lean-proofs.yml +++ b/.github/workflows/lean-proofs.yml @@ -38,8 +38,20 @@ jobs: echo "Found ${LEAN_COUNT} .lean file(s). Proceeding with build." fi - - name: Install elan (Lean version manager) + - name: Restore elan + Lake cache + id: restore-cache if: steps.check-lean-files.outputs.lean_count != '0' + uses: actions/cache@v4 + with: + path: | + formal-verification/lean/.lake + ~/.elan + key: lean-${{ runner.os }}-${{ hashFiles('formal-verification/lean/lean-toolchain', 'formal-verification/lean/lakefile.toml') }} + restore-keys: | + lean-${{ runner.os }}- + + - name: Install elan (Lean version manager) + if: steps.check-lean-files.outputs.lean_count != '0' && steps.restore-cache.outputs.cache-hit != 'true' run: | ELAN_VERSION="v4.2.1" ELAN_TARGET="x86_64-unknown-linux-gnu" @@ -61,9 +73,13 @@ jobs: exit 1 fi + # Download elan archive and its SHA-256 checksum from GitHub Releases. + # The checksum is fetched from the same release to avoid hardcoding a + # version-specific hash that would silently mismatch on elan upgrades. curl -sSfL "${ELAN_BASE_URL}/${ELAN_ARCHIVE}" -o "${ELAN_TMP_DIR}/${ELAN_ARCHIVE}" + curl -sSfL "${ELAN_BASE_URL}/${ELAN_ARCHIVE}.sha256" -o "${ELAN_TMP_DIR}/${ELAN_ARCHIVE}.sha256" cd "${ELAN_TMP_DIR}" - echo "4e717523217af592fa2d7b9c479410a31816c065d66ccbf0c2149337cfec0f5c ${ELAN_ARCHIVE}" | sha256sum -c - + sha256sum -c "${ELAN_ARCHIVE}.sha256" tar -xzf "${ELAN_ARCHIVE}" ./elan-init -y --default-toolchain "${LEAN_TOOLCHAIN}" rm -rf "${ELAN_TMP_DIR}" @@ -76,19 +92,8 @@ jobs: if: steps.check-lean-files.outputs.lean_count != '0' run: lean --version - - name: Cache Lean / Lake build artifacts - if: steps.check-lean-files.outputs.lean_count != '0' - uses: actions/cache@v4 - with: - path: | - formal-verification/lean/.lake - ~/.elan - key: lean-${{ runner.os }}-${{ hashFiles('formal-verification/lean/lean-toolchain', 'formal-verification/lean/lakefile.toml', 'formal-verification/lean/lake-manifest.json') }} - restore-keys: | - lean-${{ runner.os }}- - - name: Resolve Lean dependencies - if: steps.check-lean-files.outputs.lean_count != '0' + if: steps.check-lean-files.outputs.lean_count != '0' && steps.restore-cache.outputs.cache-hit != 'true' working-directory: formal-verification/lean run: lake update @@ -96,3 +101,19 @@ jobs: if: steps.check-lean-files.outputs.lean_count != '0' working-directory: formal-verification/lean run: lake build + + - name: Proof summary + if: steps.check-lean-files.outputs.lean_count != '0' + run: | + LEAN_DIR="formal-verification/lean/FVSquad" + THEOREM_COUNT=$(grep -rEc '^(theorem|lemma) ' "${LEAN_DIR}" --include='*.lean' 2>/dev/null || echo 0) + SORRY_COUNT=$(grep -rc '\' "${LEAN_DIR}" --include='*.lean' 2>/dev/null \ + | awk -F: '{sum += $2} END {print sum+0}') + { + echo "## 🔬 Lean Proof Summary" + echo "" + echo "| Metric | Count |" + echo "|--------|-------|" + echo "| Theorems / Lemmas declared | ${THEOREM_COUNT} |" + echo "| Lines containing \`sorry\` (stubs) | ${SORRY_COUNT} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/lean-squad.lock.yml b/.github/workflows/lean-squad.lock.yml index 6954f58bd1..9deae9aaf8 100644 --- a/.github/workflows/lean-squad.lock.yml +++ b/.github/workflows/lean-squad.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"a4e024077eb9436ce4efe916103946719ed3e016baa4223dc2e099dde06e5b32","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"fb641dfc4fa17b6bf69c676635a6d59c2f83735bf0a93f99dc1078fc11185447","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -140,6 +140,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -238,21 +241,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_dd63a5131f6da2ff_EOF' + cat << 'GH_AW_PROMPT_87c62ca9bfbf55f9_EOF' - GH_AW_PROMPT_dd63a5131f6da2ff_EOF + GH_AW_PROMPT_87c62ca9bfbf55f9_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_dd63a5131f6da2ff_EOF' + cat << 'GH_AW_PROMPT_87c62ca9bfbf55f9_EOF' Tools: add_comment(max:3), create_issue, update_issue, create_pull_request, push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_dd63a5131f6da2ff_EOF + GH_AW_PROMPT_87c62ca9bfbf55f9_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_dd63a5131f6da2ff_EOF' + cat << 'GH_AW_PROMPT_87c62ca9bfbf55f9_EOF' The following GitHub context information is available for this workflow: @@ -285,7 +288,7 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_dd63a5131f6da2ff_EOF + GH_AW_PROMPT_87c62ca9bfbf55f9_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" @@ -293,10 +296,10 @@ jobs: if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" fi - cat << 'GH_AW_PROMPT_dd63a5131f6da2ff_EOF' + cat << 'GH_AW_PROMPT_87c62ca9bfbf55f9_EOF' {{#runtime-import .github/workflows/lean-squad.md}} - GH_AW_PROMPT_dd63a5131f6da2ff_EOF + GH_AW_PROMPT_87c62ca9bfbf55f9_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -452,6 +455,17 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} + - name: Start DIFC proxy for pre-agent gh calls + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + DIFC_PROXY_POLICY: '{"allow-only":{"min-integrity":"none","repos":"all"}}' + DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.2.19' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" + - name: Set GH_REPO for proxied steps + run: | + echo "GH_REPO=${GITHUB_REPOSITORY}" >> "$GITHUB_ENV" - env: GH_TOKEN: ${{ github.token }} name: Assess FV state and compute task weights @@ -500,16 +514,17 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Stop DIFC proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -517,9 +532,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_203f36254f3bc1bd_EOF' - {"add_comment":{"max":3,"target":"*"},"create_issue":{"labels":["automation","lean-squad"],"max":1,"title_prefix":"[Lean Squad] "},"create_pull_request":{"draft":false,"labels":["automation","lean-squad"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Lean Squad] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":102400}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Lean Squad] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Lean Squad] "}} - GH_AW_SAFE_OUTPUTS_CONFIG_203f36254f3bc1bd_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_d774dcb3495ae65d_EOF' + {"add_comment":{"max":3,"target":"*"},"create_issue":{"labels":["automation","lean-squad"],"max":1,"title_prefix":"[Lean Squad] "},"create_pull_request":{"draft":false,"labels":["automation","lean-squad"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Lean Squad] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":102400}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Lean Squad] "},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Lean Squad] "}} + GH_AW_SAFE_OUTPUTS_CONFIG_d774dcb3495ae65d_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -828,8 +843,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -850,7 +863,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_027fc5d0d547ef70_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_2be40690809e0369_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -858,14 +871,18 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -891,7 +908,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_027fc5d0d547ef70_EOF + GH_AW_MCP_CONFIG_2be40690809e0369_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1116,6 +1133,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -1183,7 +1202,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Lean Squad" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1665,7 +1684,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lean-lang.org,leanlang.org,leanprover-community.github.io,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"lean-squad\"],\"max\":1,\"title_prefix\":\"[Lean Squad] \"},\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"lean-squad\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Lean Squad] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Lean Squad] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Lean Squad] \"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"lean-squad\"],\"max\":1,\"title_prefix\":\"[Lean Squad] \"},\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"lean-squad\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Lean Squad] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Lean Squad] \"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Lean Squad] \"}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lean-squad.md b/.github/workflows/lean-squad.md index e8a8cb6355..da99935ac6 100644 --- a/.github/workflows/lean-squad.md +++ b/.github/workflows/lean-squad.md @@ -49,7 +49,9 @@ checkout: tools: web-fetch: github: + lockdown: true toolsets: [default] + min-integrity: none bash: - find - grep @@ -105,6 +107,8 @@ safe-outputs: add-comment: max: 3 target: "*" + noop: + report-as-issue: false timeout-minutes: 120 diff --git a/.github/workflows/markdown-linter-report.lock.yml b/.github/workflows/markdown-linter-report.lock.yml index 90e196f6f5..35b4c94924 100644 --- a/.github/workflows/markdown-linter-report.lock.yml +++ b/.github/workflows/markdown-linter-report.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1b0f64ede7f0207fd4cb6919799276bc4e41164baf1c63801f3ef65d2451b2a6","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ba1b4f83b84a093d390a4aeefbd3a41e9a8c086b168d66978ec29e0983b929d7","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"},{"repo":"super-linter/super-linter","sha":"61abc07d755095a68f4987d1c2c3d1d64408f1f9","version":"v8.5.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -169,15 +169,15 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_61792feb2daa9b99_EOF' + cat << 'GH_AW_PROMPT_ba870ae692a22603_EOF' - GH_AW_PROMPT_61792feb2daa9b99_EOF + GH_AW_PROMPT_ba870ae692a22603_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_61792feb2daa9b99_EOF' + cat << 'GH_AW_PROMPT_ba870ae692a22603_EOF' Tools: create_issue, missing_tool, missing_data, noop @@ -209,12 +209,12 @@ jobs: {{/if}} - GH_AW_PROMPT_61792feb2daa9b99_EOF + GH_AW_PROMPT_ba870ae692a22603_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_61792feb2daa9b99_EOF' + cat << 'GH_AW_PROMPT_ba870ae692a22603_EOF' {{#runtime-import .github/workflows/markdown-linter-report.md}} - GH_AW_PROMPT_61792feb2daa9b99_EOF + GH_AW_PROMPT_ba870ae692a22603_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -421,9 +421,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f79fab6380249008_EOF' - {"create_issue":{"expires":48,"labels":["automation","code-quality"],"max":1,"title_prefix":"[linter] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_f79fab6380249008_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_577ed372493073a7_EOF' + {"create_issue":{"expires":48,"labels":["automation","code-quality"],"max":1,"title_prefix":"[linter] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_577ed372493073a7_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -618,7 +618,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_3b7b409110d6d3f1_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_4b1c0614d7dcc90a_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -659,7 +659,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_3b7b409110d6d3f1_EOF + GH_AW_MCP_CONFIG_4b1c0614d7dcc90a_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -930,7 +930,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Markdown Linter" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1237,7 +1237,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"labels\":[\"automation\",\"code-quality\"],\"max\":1,\"title_prefix\":\"[linter] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"labels\":[\"automation\",\"code-quality\"],\"max\":1,\"title_prefix\":\"[linter] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/markdown-linter-report.md b/.github/workflows/markdown-linter-report.md index 3815e74ae2..c0e2a094d2 100644 --- a/.github/workflows/markdown-linter-report.md +++ b/.github/workflows/markdown-linter-report.md @@ -18,6 +18,7 @@ safe-outputs: title-prefix: "[linter] " labels: [automation, code-quality] noop: + report-as-issue: false name: Markdown Linter timeout-minutes: 15 diff --git a/.github/workflows/pr-expert-reviewer.lock.yml b/.github/workflows/pr-expert-reviewer.lock.yml index 00fbf1c52a..b2ed5bbc01 100644 --- a/.github/workflows/pr-expert-reviewer.lock.yml +++ b/.github/workflows/pr-expert-reviewer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"a7cc86aff5c1c10ebcc03da37e6084d55f049c82ce3df1cbee731717b2b9099b","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"533723762e8705818a39fc483027e429adbfdbc422347a4a949374ccdd84ec3c","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -115,6 +115,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -184,15 +187,15 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8b656d9e77dc3b2c_EOF' + cat << 'GH_AW_PROMPT_30387acd7c2ccb6e_EOF' - GH_AW_PROMPT_8b656d9e77dc3b2c_EOF + GH_AW_PROMPT_30387acd7c2ccb6e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_8b656d9e77dc3b2c_EOF' + cat << 'GH_AW_PROMPT_30387acd7c2ccb6e_EOF' Tools: create_pull_request_review_comment(max:5), submit_pull_request_review, missing_tool, missing_data, noop @@ -224,13 +227,13 @@ jobs: {{/if}} - GH_AW_PROMPT_8b656d9e77dc3b2c_EOF + GH_AW_PROMPT_30387acd7c2ccb6e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8b656d9e77dc3b2c_EOF' + cat << 'GH_AW_PROMPT_30387acd7c2ccb6e_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/pr-expert-reviewer.md}} - GH_AW_PROMPT_8b656d9e77dc3b2c_EOF + GH_AW_PROMPT_30387acd7c2ccb6e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -425,9 +428,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bbd89c8e159b3c5b_EOF' - {"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_bbd89c8e159b3c5b_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f42b725c43d861b6_EOF' + {"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_f42b725c43d861b6_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -643,7 +646,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_872d9c946494187b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_7f223445d215b7a5_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -651,6 +654,7 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "pull_requests,repos" @@ -687,7 +691,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_872d9c946494187b_EOF + GH_AW_MCP_CONFIG_7f223445d215b7a5_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -941,7 +945,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Expert Code Reviewer 🧠" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1275,7 +1279,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pr-expert-reviewer.md b/.github/workflows/pr-expert-reviewer.md index 096966a42c..11cbb7f774 100644 --- a/.github/workflows/pr-expert-reviewer.md +++ b/.github/workflows/pr-expert-reviewer.md @@ -16,11 +16,13 @@ permissions: tools: cache-memory: true github: + lockdown: true toolsets: [pull_requests, repos] min-integrity: none safe-outputs: - noop: {} + noop: + report-as-issue: false create-pull-request-review-comment: max: 5 side: "RIGHT" diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index b8e5da4b8e..e7d1125e4e 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c7aeb0ff49749532de284f0c637e70db20f94b66870f01f531d3f9aa82a8c067","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f7cf3416964c9df2c14d268a8cdc5c9c5343e7bb2868040f71b68a860d1db7eb","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -61,6 +61,9 @@ name: "PR Nitpick Reviewer 🔍" types: - created - edited + pull_request_target: + types: + - opened permissions: {} @@ -72,7 +75,7 @@ run-name: "PR Nitpick Reviewer 🔍" jobs: activation: needs: pre_activation - if: "needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit'))" + if: "needs.pre_activation.outputs.activated == 'true' && ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit')) || (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request_review_comment')))" runs-on: ubuntu-slim permissions: actions: read @@ -120,6 +123,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -216,15 +222,15 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF' + cat << 'GH_AW_PROMPT_8fd9f9e96bb77841_EOF' - GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF + GH_AW_PROMPT_8fd9f9e96bb77841_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF' + cat << 'GH_AW_PROMPT_8fd9f9e96bb77841_EOF' Tools: create_pull_request_review_comment(max:10), submit_pull_request_review, missing_tool, missing_data, noop @@ -256,16 +262,16 @@ jobs: {{/if}} - GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF + GH_AW_PROMPT_8fd9f9e96bb77841_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF' + cat << 'GH_AW_PROMPT_8fd9f9e96bb77841_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/pr-nitpick-reviewer.md}} - GH_AW_PROMPT_ca9cbbd39a00d4b8_EOF + GH_AW_PROMPT_8fd9f9e96bb77841_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -464,9 +470,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_48b659154f4f5c15_EOF' - {"create_pull_request_review_comment":{"max":10,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_48b659154f4f5c15_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3a68a29f5132df88_EOF' + {"create_pull_request_review_comment":{"max":10,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_3a68a29f5132df88_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -682,7 +688,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_fc6f71015fe039c2_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_449df0c6684de6de_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -690,6 +696,7 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "pull_requests,repos" @@ -726,7 +733,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fc6f71015fe039c2_EOF + GH_AW_MCP_CONFIG_449df0c6684de6de_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -981,7 +988,7 @@ jobs: GH_AW_WORKFLOW_NAME: "PR Nitpick Reviewer 🔍" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1242,7 +1249,7 @@ jobs: await main(); pre_activation: - if: "github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit')" + if: "(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/nit ') || startsWith(github.event.comment.body, '/nit\n') || github.event.comment.body == '/nit')) || (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request_review_comment'))" runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }} @@ -1346,7 +1353,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pr-nitpick-reviewer.md b/.github/workflows/pr-nitpick-reviewer.md index 0bece91fa3..de010256d5 100644 --- a/.github/workflows/pr-nitpick-reviewer.md +++ b/.github/workflows/pr-nitpick-reviewer.md @@ -8,6 +8,13 @@ on: slash_command: name: nit events: [pull_request_comment, pull_request_review_comment] + # pull_request_target is intentional: this workflow needs write access to post review comments, + # which requires pull_request_target (not pull_request) to work on fork PRs. + # Security: the agent only posts review comments (no code changes). XPIA risk from adversarial + # fork PR content is mitigated by the gh-aw XPIA prompt and locked-down permissions: {} in + # the compiled workflow. + pull_request_target: + types: [opened] permissions: contents: read @@ -15,12 +22,18 @@ permissions: actions: read tools: - cache-memory: true + cache-memory: + - true # default (workflow-private) cache for nitpick-patterns.json, conventions.json, etc. + - id: repo-history + key: repo-history # shared cache produced by the repo-historian workflow github: + lockdown: true toolsets: [pull_requests, repos] min-integrity: none safe-outputs: + noop: + report-as-issue: false create-pull-request-review-comment: max: 10 side: "RIGHT" @@ -69,6 +82,10 @@ Use the cache memory at `/tmp/gh-aw/cache-memory/` to: - Read previous nitpick patterns from `/tmp/gh-aw/cache-memory/nitpick-patterns.json` - Review user instructions from `/tmp/gh-aw/cache-memory/user-preferences.json` - Note team coding conventions from `/tmp/gh-aw/cache-memory/conventions.json` +- Check repository history insights from `/tmp/gh-aw/cache-memory-repo-history/repo-history.json` if it is present in the shared `repo-history` cache. If present, use it to: + - Prioritize review effort on high-churn files and high-risk directories + - Skip deep analysis of stable, low-churn areas when the PR is large + - Be aware of recurring style patterns that were flagged in recent PRs ### Step 2: Deduplication Check diff --git a/.github/workflows/pr-test-expert-reviewer.lock.yml b/.github/workflows/pr-test-expert-reviewer.lock.yml index 68e5233af8..51c3f6d369 100644 --- a/.github/workflows/pr-test-expert-reviewer.lock.yml +++ b/.github/workflows/pr-test-expert-reviewer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f8fc48265a8c5c3a9be693c790ba79bdbde6fdffa49bc0e832002b8563754032","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"64df40f0d0acadcb8644e582a6d7e5dfe443ff03675d9aa65867ebc043f4dc22","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -115,6 +115,9 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" + GITHUB_MCP_LOCKDOWN_EXPLICIT: "true" + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 with: script: | @@ -184,15 +187,15 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF' + cat << 'GH_AW_PROMPT_70c5ecd26374a2f8_EOF' - GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF + GH_AW_PROMPT_70c5ecd26374a2f8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF' + cat << 'GH_AW_PROMPT_70c5ecd26374a2f8_EOF' Tools: create_pull_request_review_comment(max:7), submit_pull_request_review, missing_tool, missing_data, noop @@ -224,13 +227,13 @@ jobs: {{/if}} - GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF + GH_AW_PROMPT_70c5ecd26374a2f8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF' + cat << 'GH_AW_PROMPT_70c5ecd26374a2f8_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/pr-test-expert-reviewer.md}} - GH_AW_PROMPT_d2f1ac8bcaff7c0e_EOF + GH_AW_PROMPT_70c5ecd26374a2f8_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -425,9 +428,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7533b297e1b54403_EOF' - {"create_pull_request_review_comment":{"max":7,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_7533b297e1b54403_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8edb9fc05af001ec_EOF' + {"create_pull_request_review_comment":{"max":7,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{},"submit_pull_request_review":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_8edb9fc05af001ec_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -643,7 +646,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_e252b733e12a3bf7_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_05e58805a485b11d_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -651,6 +654,7 @@ jobs: "container": "ghcr.io/github/github-mcp-server:v0.32.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_LOCKDOWN_MODE": "1", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "pull_requests,repos" @@ -687,7 +691,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_e252b733e12a3bf7_EOF + GH_AW_MCP_CONFIG_05e58805a485b11d_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -941,7 +945,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Test Expert Reviewer 🧪" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1275,7 +1279,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":7,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request_review_comment\":{\"max\":7,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pr-test-expert-reviewer.md b/.github/workflows/pr-test-expert-reviewer.md index 88b8820230..4c8883c1c3 100644 --- a/.github/workflows/pr-test-expert-reviewer.md +++ b/.github/workflows/pr-test-expert-reviewer.md @@ -16,11 +16,13 @@ permissions: tools: cache-memory: true github: + lockdown: true toolsets: [pull_requests, repos] min-integrity: none safe-outputs: - noop: {} + noop: + report-as-issue: false create-pull-request-review-comment: max: 7 side: "RIGHT" diff --git a/.github/workflows/repo-historian.lock.yml b/.github/workflows/repo-historian.lock.yml new file mode 100644 index 0000000000..0ed3495da5 --- /dev/null +++ b/.github/workflows/repo-historian.lock.yml @@ -0,0 +1,1284 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6e61e6c36d582aafb2d74c74eb53204a3c44e8d725e694463506b1bce41df3be","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.3). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Scheduled workflow that analyzes repository history and builds a knowledge base in cache-memory for the PR reviewer workflows to consume. Tracks high-churn files, reverted commits, CI failure patterns, and recurring review feedback. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.20 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.20 +# - ghcr.io/github/gh-aw-mcpg:v0.2.19 +# - ghcr.io/github/github-mcp-server:v0.32.0 +# - node:lts-alpine + +name: "Repo Historian 📜" +"on": + schedule: + - cron: "13 5 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Repo Historian 📜" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.21" + GH_AW_INFO_AGENT_VERSION: "1.0.21" + GH_AW_INFO_CLI_VERSION: "v0.68.3" + GH_AW_INFO_WORKFLOW_NAME: "Repo Historian 📜" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.20" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_WORKFLOW_FILE: "repo-historian.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.3" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_3fda397a4a4a046d_EOF' + + GH_AW_PROMPT_3fda397a4a4a046d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_3fda397a4a4a046d_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + GH_AW_PROMPT_3fda397a4a4a046d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_auto_create_issue.md" + cat << 'GH_AW_PROMPT_3fda397a4a4a046d_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_3fda397a4a4a046d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_3fda397a4a4a046d_EOF' + + {{#runtime-import .github/workflows/repo-historian.md}} + GH_AW_PROMPT_3fda397a4a4a046d_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: repohistorian + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-9f0b69b3-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-9f0b69b3-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_13b857047ee09c84_EOF' + {"create_issue":{"labels":["repo-historian"],"max":1,"title_prefix":"[repo-historian]"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_13b857047ee09c84_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[repo-historian]\". Labels [\"repo-historian\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_febfd8241056b480_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "pull_requests,repos,issues" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_febfd8241056b480_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(git:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 10 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.68.3 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-repo-historian" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "repo-historian" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "10" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + WORKFLOW_NAME: "Repo Historian 📜" + WORKFLOW_DESCRIPTION: "Scheduled workflow that analyzes repository history and builds a knowledge base in cache-memory for the PR reviewer workflows to consume. Tracks high-churn files, reverted commits, CI failure patterns, and recurring review feedback." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.68.3 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/repo-historian" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "repo-historian" + GH_AW_WORKFLOW_NAME: "Repo Historian 📜" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"repo-historian\"],\"max\":1,\"title_prefix\":\"[repo-historian]\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: > + always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && + needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: repohistorian + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-9f0b69b3-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/repo-historian.md b/.github/workflows/repo-historian.md new file mode 100644 index 0000000000..af24e2db22 --- /dev/null +++ b/.github/workflows/repo-historian.md @@ -0,0 +1,185 @@ +--- +description: > + Scheduled workflow that analyzes repository history and builds a knowledge base + in cache-memory for the PR reviewer workflows to consume. Tracks high-churn files, + reverted commits, CI failure patterns, and recurring review feedback. + +on: + schedule: daily + +permissions: + contents: read + pull-requests: read + issues: read + actions: read + +tools: + cache-memory: + - id: repo-history + key: repo-history + github: + toolsets: [pull_requests, repos, issues] + min-integrity: none + bash: [git, grep, sort, uniq, wc, head, tail, cat, date, jq] + +safe-outputs: + noop: + report-as-issue: false + +timeout-minutes: 10 +--- + +# Repo Historian 📜 + +You are a repository analyst that builds a knowledge base about the repository's recent history. Your output is structured data in cache-memory files that other reviewer workflows consume to make better decisions. + +You do **NOT** review code. You do **NOT** create PRs or issues. You produce data files only. + +## Your Mission + +Analyze the repository's recent activity and update cache-memory files with structured insights that help PR reviewers prioritize their analysis. + +### Step 1: Load Existing Knowledge + +Read existing cache-memory files to understand what you already know: + +- `/tmp/gh-aw/cache-memory-repo-history/repo-history.json` — previous run's output +- `/tmp/gh-aw/cache-memory/architecture.json` — architectural notes from expert reviewer +- `/tmp/gh-aw/cache-memory/flaky-tests.json` — flaky test patterns from test reviewer + +### Step 2: Analyze Recent Merged PRs + +Use the GitHub tools to fetch PRs merged in the last 7 days: + +For each merged PR, record: + +- **Files changed** — which files/directories were touched +- **PR size** — number of files and lines changed +- **Had review comments requesting changes** — indicates areas where mistakes happen +- **Labels** — to categorize change types (bug fix, feature, refactoring, dependencies) + +Store only non-identifying metadata needed for reviewer prioritization, such as file paths, counts, and aggregate patterns. + +### Step 3: Identify High-Churn Files + +Compute file churn from the last 30 days: + +```bash +git log --since="30 days ago" --pretty=format: --name-only --no-merges | sort | uniq -c | sort -rn | head -30 +``` + +**High-churn** = changed in 3+ non-merge commits within 30 days (matching the `--no-merges` flag above). These files deserve extra scrutiny because: + +- Frequent changes suggest the code is actively evolving and may have incomplete designs +- More changes = more opportunities for regressions +- If the file was also reverted, it's doubly risky + +### Step 4: Detect Reverted Commits + +Search for revert commits in the last 30 days: + +```bash +git log --since="30 days ago" --grep="Revert" --pretty=format:"%H %s" --no-merges +``` + +For each revert: + +- Record the original commit that was reverted +- Record the files it touched +- Flag those files as **high-risk** — a previous change was backed out, meaning the area is tricky + +### Step 5: Analyze CI Failure Patterns + +Use the GitHub tools to check recent workflow runs: + +- Look for `pull_request` workflow runs with `conclusion: failure` in the last 14 days +- Correlate failures with the files changed in the corresponding PR +- Build a map of `file → CI failure count` to identify fragile areas + +### Step 6: Track Review Feedback Patterns + +Analyze merged PRs that had `REQUEST_CHANGES` reviews: + +- What categories of issues were flagged? (Look for `[Correctness]`, `[Threading]`, etc. tags from the expert reviewer) +- Which directories had the most review feedback? +- Were there recurring patterns? (e.g., "missing ConfigureAwait" appearing in 3 PRs in Platform/) + +### Step 7: Write Cache-Memory Output + +Write a single structured file: `/tmp/gh-aw/cache-memory-repo-history/repo-history.json` + +The file should have this structure: + +```json +{ + "last_updated": "2026-04-27T00:00:00Z", + "analysis_window_days": 30, + "high_churn_files": [ + { + "path": "src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs", + "change_count_30d": 5, + "was_reverted": false, + "ci_failure_correlation": 0, + "review_feedback_count": 2 + } + ], + "reverted_files": [ + { + "path": "src/some/file.cs", + "revert_commit": "abc123", + "original_commit": "def456", + "revert_date": "2026-04-20" + } + ], + "ci_fragile_areas": [ + { + "directory": "src/Platform/", + "failure_count_14d": 3, + "common_failure_patterns": ["Build Linux Debug timeout", "Test flakiness in acceptance tests"] + } + ], + "recurring_review_patterns": [ + { + "pattern": "missing ConfigureAwait(false)", + "category": "Threading", + "occurrences": 3, + "directories": ["src/Platform/", "src/Adapter/"] + } + ], + "directory_risk_scores": { + "src/Platform/Microsoft.Testing.Platform/CommandLine/": 8, + "src/Adapter/MSTest.TestAdapter/": 5, + "src/TestFramework/TestFramework/": 3, + "test/": 1 + } +} +``` + +**Risk score** (1-10) is computed by first calculating the raw score +`churn_weight * 3 + revert_weight * 4 + ci_failure_weight * 2 + review_feedback_weight * 1`, +then clamping the result to the 1-10 range. + +### Step 8: Invoke noop + +After writing the cache-memory file, always invoke `noop` to signal completion: + +```json +{"noop": {"message": "Repo history analysis complete: analyzed N merged PRs, identified M high-churn files, K reverted areas, J CI-fragile directories."}} +``` + +## What Consumers Do With This Data + +This workflow does NOT act on the data. The PR reviewers read `repo-history.json` from the shared `repo-history` cache-memory (at `/tmp/gh-aw/cache-memory-repo-history/repo-history.json`) and use it to: + +| Consumer | How it uses history | +| --- | --- | +| **Expert Reviewer** | Applies extra scrutiny to high-churn files; checks reverted areas more carefully for the same class of bug | +| **Nitpick Reviewer** | Prioritizes reviews on high-risk directories; skips deep analysis of stable, low-churn areas | +| **Test Expert Reviewer** | Cross-references CI failures with test file changes; flags test changes in fragile areas | + +## Important Notes + +- **No PRs or issues** — This workflow only writes cache-memory. It must not create issues, PRs, or comments. +- **Idempotent** — Running twice produces the same output (or more recent data). Safe to re-run anytime. +- **Graceful degradation** — If GitHub API calls fail (rate limits, permissions), write what you can and note gaps in the output. +- **Privacy** — Do not store commit messages, PR descriptions, or comment bodies. Only store file paths, counts, and patterns. diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index 84f27ad34b..50b74e8657 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c3190a7cf249bce6a522d195e666c8eab1c6227756ad1526ac2b6520dac17fc9","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"57ad754731f4e531db1cdd4abb41bdfb53cfe8d80456cd5f3d5e32523b9a17af","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -166,15 +166,15 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_5f012745a8f65b64_EOF' + cat << 'GH_AW_PROMPT_15fe8bd2b32ea907_EOF' - GH_AW_PROMPT_5f012745a8f65b64_EOF + GH_AW_PROMPT_15fe8bd2b32ea907_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt_multi.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_5f012745a8f65b64_EOF' + cat << 'GH_AW_PROMPT_15fe8bd2b32ea907_EOF' Tools: create_issue, missing_tool, missing_data, noop @@ -206,12 +206,12 @@ jobs: {{/if}} - GH_AW_PROMPT_5f012745a8f65b64_EOF + GH_AW_PROMPT_15fe8bd2b32ea907_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_5f012745a8f65b64_EOF' + cat << 'GH_AW_PROMPT_15fe8bd2b32ea907_EOF' {{#runtime-import .github/workflows/repository-quality-improver.md}} - GH_AW_PROMPT_5f012745a8f65b64_EOF + GH_AW_PROMPT_15fe8bd2b32ea907_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -406,9 +406,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2c9fa581dd0d2699_EOF' - {"create_issue":{"expires":48,"labels":["quality","automated-analysis"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_2c9fa581dd0d2699_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_689c8157814db3b6_EOF' + {"create_issue":{"expires":48,"labels":["quality","automated-analysis"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_689c8157814db3b6_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -603,7 +603,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_adf64dde33d3b1ee_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_f546a6fad093d9fd_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -644,7 +644,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_adf64dde33d3b1ee_EOF + GH_AW_MCP_CONFIG_f546a6fad093d9fd_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -916,7 +916,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Repository Quality Improver" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1223,7 +1223,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"labels\":[\"quality\",\"automated-analysis\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"expires\":48,\"labels\":[\"quality\",\"automated-analysis\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/repository-quality-improver.md b/.github/workflows/repository-quality-improver.md index ae49e144fb..68488d67e8 100644 --- a/.github/workflows/repository-quality-improver.md +++ b/.github/workflows/repository-quality-improver.md @@ -34,6 +34,8 @@ tools: - default safe-outputs: + noop: + report-as-issue: false create-issue: expires: 2d labels: [quality, automated-analysis] diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000000..108698a97e --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,24 @@ +{ + "threshold": 100, + "minLines": 6, + "minTokens": 50, + "reporters": ["json", "consoleFull"], + "output": "artifacts/jscpd", + "ignore": [ + "**/bin/**", + "**/obj/**", + "**/artifacts/**", + "**/node_modules/**", + "**/*.Designer.cs", + "**/*.g.cs", + "**/*.xlf", + "**/*.resx", + "**/PublicAPI.Shipped.txt", + "**/PublicAPI.Unshipped.txt", + "**/test/**", + "**/samples/**", + "**/formal-verification/**" + ], + "format": ["csharp"], + "absolute": false +} diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 13ab1ef4b0..b6251a2687 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -13,17 +13,17 @@ https://github.com/dotnet/arcade f5d199ccaf897f1ab275ce683b4151be403458b5 - + https://dev.azure.com/devdiv/DevDiv/_git/vs-code-coverage - b2f83b8e6030a4e36a07d452d9ef561b8fed7ce6 + d6522e07c3e2e225c6b7b8ad1a081c301f8d265d - + https://github.com/microsoft/testfx - cc9d543fc781af1d0dbabfe61c8cabef117d11b0 + 88f30ff859b8e04d8012805d2ff94e83ffdffc16 - + https://github.com/microsoft/testfx - cc9d543fc781af1d0dbabfe61c8cabef117d11b0 + 88f30ff859b8e04d8012805d2ff94e83ffdffc16 diff --git a/eng/Versions.props b/eng/Versions.props index 906d669763..001c821fe9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,9 +8,9 @@ 11.0.0-beta.26227.3 - 18.7.0-preview.26227.2 + 18.8.0-preview.26228.2 - 4.3.0-preview.26224.7 - 2.3.0-preview.26224.7 + 4.3.0-preview.26228.1 + 2.3.0-preview.26228.1 diff --git a/formal-verification/TARGETS.md b/formal-verification/TARGETS.md index 12b9d0277a..bc67adc214 100644 --- a/formal-verification/TARGETS.md +++ b/formal-verification/TARGETS.md @@ -21,7 +21,7 @@ | 3 | `CommandLineParser.ParseOptionAndSeparators` | `src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs` | 1 | Identified | — | | 4 | `CommandLineOptionsValidator` arity validation | `src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs` | 1 | Identified | — | | 5 | `CommandLineParseResult.Equals` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ParseResult.cs` | 1 | Identified | — | -| 6 | `ResponseFileHelper.SplitCommandLine` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ResponseFileHelper.cs` | 1 | Identified | — | +| 6 | `ResponseFileHelper.SplitCommandLine` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ResponseFileHelper.cs` | 2 | Informal spec extracted | — | | 7 | `TreeNodeFilter.MatchFilterPattern` | `src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs` | 1 | Identified | — | ## Priority Order diff --git a/formal-verification/specs/commandlineparseresult_equals_informal.md b/formal-verification/specs/commandlineparseresult_equals_informal.md new file mode 100644 index 0000000000..ce6a80de32 --- /dev/null +++ b/formal-verification/specs/commandlineparseresult_equals_informal.md @@ -0,0 +1,185 @@ +# Informal Specification — `CommandLineParseResult.Equals` + +> 🔬 **Lean Squad** — auto-generated and maintained by the Lean Squad FV agent. + +## Target + +- **Type**: `sealed class CommandLineParseResult(string? toolName, IReadOnlyList options, IReadOnlyList errors) : IEquatable` +- **Method**: `bool Equals(CommandLineParseResult? other)` +- **Namespace**: `Microsoft.Testing.Platform.CommandLine` +- **File**: `src/Platform/Microsoft.Testing.Platform/CommandLine/ParseResult.cs` +- **Phase**: 2 — Informal Spec + +--- + +## Purpose + +`CommandLineParseResult.Equals` implements structural equality for the result of parsing a command-line. Two results are equal if and only if they represent the same parsed command line: same tool name, same errors (order-sensitive), and same options with the same arguments (order-sensitive). + +The method is used by equality comparisons in tests and validation pipelines that compare parse results to expected values. + +--- + +## Data Model + +``` +CommandLineParseResult = { + ToolName : string? (nullable) + Options : IReadOnlyList + Errors : IReadOnlyList +} + +CommandLineParseOption = { + Name : string + Arguments : string[] (array, positional) +} +``` + +An empty parse result `Empty` is defined as `new(null, [], [])`. + +--- + +## Method Signature + +```csharp +public bool Equals(CommandLineParseResult? other) +``` + +--- + +## Preconditions + +- `this` is a valid, fully-constructed `CommandLineParseResult` (non-null when the instance method is executing, because instance methods cannot be invoked on a null reference). +- `other` may be `null` (nullable parameter). +- For a valid `CommandLineParseResult` instance, both `Options` and `Errors` are expected to be non-null; passing `null` for either is invalid input and not prevented by the primary constructor at runtime. +- For a valid `CommandLineParseOption` instance, `Arguments` is expected to be a non-null `string[]`; passing `null` is invalid input and not prevented by the primary constructor at runtime. + +--- + +## Algorithm (from source) + +1. If `other` is `null`, return `false`. +2. If `ReferenceEquals(this, other)`, return `true` (reference equality short-circuit). +3. If `ToolName != other.ToolName`, return `false` (string comparison via `!=`). +4. If `Errors.Count != other.Errors.Count`, return `false`. +5. For each index `i` in `0..` vs `List T`**: Model as Lean's `List` for spec purposes. The actual runtime type (array, list, etc.) is abstracted away. +4. **`string[]` vs `List String`**: Model `string[]` as `List String`. +5. **Case-sensitive vs OrdinalIgnoreCase**: C# `string !=` uses ordinal comparison; model as Lean `String.decEq`. Note the contrast with `IsOptionSet` which uses `OrdinalIgnoreCase`. +6. **Self-equality when `ReferenceEquals`**: In Lean, there is no reference equality — pure structural equality is the natural model. The reference check is an optimisation; proofs should hold without it. + +--- + +## Approximations for Lean Model + +- Model `string` as Lean `String` with native `DecidableEq String`. +- Model `string?` as `Option String`; `null` maps to `none`. +- Model `IReadOnlyList` as `List T`. +- Model `string[]` as `List String`. +- Ignore `GetHashCode` (not needed for equality proofs). +- Ignore `object.Equals` overload (adds type-erasure complexity without insight). +- Ignore `==` / `!=` operator semantics; `CommandLineParseResult` does not define custom equality operators in the source. +- The reference-equality short-circuit is invisible to the Lean model; proofs are about value equality only. diff --git a/formal-verification/specs/responsefilehelper_splitcommandline_informal.md b/formal-verification/specs/responsefilehelper_splitcommandline_informal.md new file mode 100644 index 0000000000..fe9803a4eb --- /dev/null +++ b/formal-verification/specs/responsefilehelper_splitcommandline_informal.md @@ -0,0 +1,225 @@ +# Informal Specification — `ResponseFileHelper.SplitCommandLine` + +> 🔬 **Lean Squad** — auto-generated and maintained by the Lean Squad FV agent. + +## Target + +- **Method**: `public static IEnumerable SplitCommandLine(string commandLine)` +- **Class**: `ResponseFileHelper` (internal static class) +- **Namespace**: `Microsoft.Testing.Platform` (internal) +- **File**: `src/Platform/Microsoft.Testing.Platform/CommandLine/ResponseFileHelper.cs` +- **Phase**: 2 — Informal Spec +- **Upstream reference**: [dotnet/command-line-api `StringExtensions.cs`](https://github.com/dotnet/command-line-api/blob/feb61c7f328a2401d74f4317b39d02126cfdfe24/src/System.CommandLine/Parsing/StringExtensions.cs#L349) + +--- + +## Purpose + +`SplitCommandLine` tokenises a single command-line string into a sequence of argument strings. It is used when reading response files (`@file` arguments): each line of the response file is first stripped of leading/trailing whitespace and `#`-prefixed comment lines (in the caller `SplitLine`), then passed to `SplitCommandLine` to be tokenised. + +The function handles two categories of characters: +- **Whitespace** — token delimiter outside of quoted regions +- **Double-quote** (`"`) — begins and ends quoted regions where whitespace is literal + +--- + +## Data Model + +**Input**: a single `string commandLine` + +**Output**: an `IEnumerable` — a lazy sequence of token strings + +**Internal state machine** (two independent variables): + +| Variable | Enum | Values | Meaning | +|---|---|---|---| +| `seeking` | `Boundary` | `TokenStart`, `WordEnd` | whether we are looking for the start of a new token or for the end of the current token | +| `seekingQuote` | `Boundary` | `QuoteStart`, `QuoteEnd` | whether we are outside (`QuoteStart`) or inside (`QuoteEnd`) a double-quoted region | + +Initial state: `(TokenStart, QuoteStart)` — not in a word, not in a quote. + +**Token accumulation**: `startTokenIndex` is the start of the current token in the input string. The function accumulates characters from `startTokenIndex` to `pos`. On yield, it emits `commandLine[startTokenIndex .. pos].Replace("\"", "")`. + +--- + +## Transition Rules (per character) + +### Whitespace character (space, tab, etc.) + +| `seeking` | `seekingQuote` | Action | +|---|---|---| +| `WordEnd` | `QuoteStart` | Yield current token; `startTokenIndex = pos`; `seeking = TokenStart` | +| `TokenStart` | `QuoteStart` | `startTokenIndex = pos` (advance past whitespace) | +| any | `QuoteEnd` | Literal character — part of current quoted token; no state change | + +### Double-quote character (`"`) + +| `seeking` | `seekingQuote` | Action | +|---|---|---| +| `TokenStart` | `QuoteStart` | Enter quoted region: `startTokenIndex = pos + 1`; `seekingQuote = QuoteEnd` | +| `TokenStart` | `QuoteEnd` | Close quoted region: yield current token; `startTokenIndex = pos`; `seekingQuote = QuoteStart` | +| `WordEnd` | `QuoteStart` | Toggle into quote mid-word: `seekingQuote = QuoteEnd` | +| `WordEnd` | `QuoteEnd` | Toggle out of quote mid-word: `seekingQuote = QuoteStart` | + +### Any other character (non-whitespace, non-quote) + +| `seeking` | `seekingQuote` | Action | +|---|---|---| +| `TokenStart` | `QuoteStart` | Start unquoted token: `seeking = WordEnd`; `startTokenIndex = pos` | +| other combinations | — | No state change (character is part of current token) | + +### End of input + +| `seeking` | Action | +|---|---| +| `TokenStart` | Nothing (no pending token) | +| `WordEnd` | Yield remaining token (`commandLine[startTokenIndex .. end].Replace("\"", "")`) | + +Note: if `seeking == TokenStart` at end-of-input, even if `seekingQuote == QuoteEnd` (unclosed quote), **no token is yielded**. This is a potential bug/edge case — an unclosed quote starting right at end of input yields nothing. + +--- + +## Preconditions + +- `commandLine` is a non-null string (enforced by C# type system; no null-guard in method) +- The function is designed to receive a non-empty, non-comment line (callers pre-strip) +- Any string is a valid input (the function is total) + +--- + +## Postconditions / Properties + +### Property Group 1 — Trivial inputs + +1. **Empty string**: `SplitCommandLine("") = []` (empty sequence) +2. **Whitespace-only**: `SplitCommandLine(" ") = []` (pure whitespace, no tokens) +3. **Single word**: `∀ w, (w contains no whitespace and no `"`) → SplitCommandLine(w) = [w]` +4. **Leading/trailing whitespace**: `SplitCommandLine(" hello ") = ["hello"]` + +### Property Group 2 — Whitespace splitting + +5. **Whitespace delimiter**: `SplitCommandLine("a b") = ["a", "b"]` +6. **Multiple spaces treated as one delimiter**: `SplitCommandLine("a b") = ["a", "b"]` +7. **N words**: a string of N whitespace-separated non-empty words (no quotes) yields exactly N tokens +8. **Tabs split identically to spaces**: `char.IsWhiteSpace` handles tabs, newlines, etc. + +### Property Group 3 — Quote handling + +9. **Quoted grouping**: `SplitCommandLine("\"a b\"") = ["a b"]` (space inside quotes is literal) +10. **Quote stripping**: output tokens never contain `"` characters + - `∀ s, ∀ token ∈ SplitCommandLine(s), token.IndexOf('"') == -1` +11. **Empty quoted string**: `SplitCommandLine("\"\"") = [""]` (yields one empty token) +12. **Quoted whitespace preserved**: `SplitCommandLine("\" \"") = [" "]` (space inside quotes is a token) +13. **Adjacent quoted segments merge**: `SplitCommandLine("\"a\"\"b\"") = ["a", "b"]` — NOTE: separate yields because closing `"` triggers a yield when `seeking == TokenStart` +14. **Mid-word quote embedding**: `SplitCommandLine("abc\"def\"ghi") = ["abcdefghi"]` (quote stripped, no split) +15. **Unquoted + quoted merge (open token)**: `SplitCommandLine("abc\"def\"") = ["abcdef"]` +16. **Quoted then unquoted**: `SplitCommandLine("\"abc\"def") = ["abc", "def"]` + - The closing `"` yields the quoted token "abc", then 'd' starts a new unquoted token + +### Property Group 4 — Structural invariants + +17. **No quotes in output**: all output tokens have `token.IndexOf('"') == -1` +18. **Token non-emptiness from unquoted text**: a token started by a non-whitespace, non-quote character is non-empty +19. **Empty token only from empty quotes**: the only way to emit an empty string token is via `""` +20. **Determinism**: the function is pure (no side effects, no random or I/O) — same input always yields same output +21. **Output count ≥ 0**: the result is always a non-negative number of tokens + +### Property Group 5 — Composition + +22. **Single word round-trip (no quotes)**: if `w` has no whitespace and no `"`, then `SplitCommandLine(w) = [w]` +23. **Concatenation with space**: `SplitCommandLine(a + " " + b)` where `a, b` have no whitespace/quotes → `[a, b]` + +--- + +## Edge Cases + +| Input | Expected output | Notes | +|-------|----------------|-------| +| `""` | `[]` | Empty string; loop never executes | +| `" "` | `[]` | Whitespace-only | +| `"a"` | `["a"]` | Single character | +| `"a b c"` | `["a", "b", "c"]` | Multiple words | +| `"\"hello world\""` | `["hello world"]` | Quoted with space | +| `"\"\""` | `[""]` | Empty quoted string | +| `"\" \""` | `[" "]` | Quoted single space | +| `"abc\"def\"ghi"` | `["abcdefghi"]` | Embedded quote in word | +| `"\"abc\"def"` | `["abc", "def"]` | Closing quote mid-string creates two tokens | +| `"\"abc\" \"def\""` | `["abc", "def"]` | Two quoted words | +| `"\"abc\""` (unclosed: `"abc`) | `[]` | Unclosed quote, `seeking==TokenStart` at end → no token emitted! | +| `"abc\""` (unclosed: `abc"`) | `["abc"]` | Word started before quote; toggle to QuoteEnd mid-word; end yields word | +| `"#comment"` | `["#comment"]` | Comments are NOT stripped by this method (handled by caller) | +| `"\t\ta\tb"` | `["a", "b"]` | Tab characters as delimiters | + +--- + +## Confirmed Design Properties + +1. The function is a **tokeniser** (lexer), not a parser. It produces flat token sequences. +2. It handles **at most one level** of quoting — there is no escape sequence support (no `\"` inside a quoted string). +3. Quotes are **stripped** from output — the consumer sees unquoted text. +4. The function is **lazy** (`IEnumerable` with `yield return`) — tokens are produced on demand. +5. The function calls `string.Replace("\"", "")` on **each token substring** to strip quotes. This means even edge cases like `"a\"b"` (unmatched quote in word) have quotes stripped. + +--- + +## Potential Issues / Open Questions + +### Issue 1 — No escape sequence support + +There is no way to include a literal `"` in a token. This is by design (matches the upstream command-line-api implementation), but it means the tokeniser is not a full POSIX shell tokeniser. + +### Issue 2 — Unclosed quote at start of input + +`SplitCommandLine("\"abc")` (opening `"` but no closing `"`): +- pos=0: `"` → seeking==TokenStart, seekingQuote==QuoteStart → `startTokenIndex=1`, `seekingQuote=QuoteEnd` +- pos=1,2,3: 'a','b','c' → no action (not whitespace, not `"`, and seeking==TokenStart so unquoted-char rule doesn't fire) +- End of input: `seeking == TokenStart` → no yield + +**Result**: `[]` — the entire input is silently discarded! This is a potential correctness issue: an unclosed quoted string is treated as if it were empty. + +### Issue 3 — Unclosed quote mid-word + +`SplitCommandLine("abc\"def")`: +- pos=0: 'a' → seeking=WordEnd, startTokenIndex=0 +- pos=1,2: 'b','c' +- pos=3: `"` → seeking==WordEnd → toggles to QuoteEnd +- pos=4,5,6: 'd','e','f' → no action +- End: seeking==WordEnd → yield Substring(0,7).Replace(...)="abcdef" + +**Result**: `["abcdef"]` — the unclosed quote is stripped, and content is included. This is probably correct for a response-file tokeniser, but the asymmetry with Issue 2 (opening quote at TokenStart) is notable. + +### Issue 4 — `"foo""bar"` emits two tokens + +`SplitCommandLine("\"foo\"\"bar\"")` → `["foo", "bar"]` + +When the closing `"` of `"foo"` fires while `seeking==TokenStart`, it yields and resets. This means that two adjacent quoted strings are NOT concatenated; they are separate tokens. This contrasts with some shell implementations where `"foo""bar"` → `foobar`. + +### Issue 5 — No unit tests in the test suite + +The codebase has no tests for `SplitCommandLine` directly. The behaviour described above is derived purely from code analysis. + +--- + +## Invariants for Lean Formalisation + +1. **Quote-free output**: `∀ t ∈ result, '\"' ∉ t.toList` +2. **Empty input → empty result**: `splitCommandLine "" = []` +3. **Whitespace-free tokens from unquoted input**: if `commandLine` has no `"`, then each output token has no whitespace characters +4. **Quote grouping**: text between a matched `"..."` pair appears as a contiguous substring of a single token (though that token may have additional characters from surrounding unquoted text — only if `seeking == WordEnd` before the opening `"`) +5. **Token count stability**: repeated calls with the same input yield the same count and token sequence + +--- + +## Approximations for Lean Model + +- Model `char.IsWhiteSpace` as a decidable predicate `isWhitespace : Char → Bool` with `isWhitespace ' ' = true`, `isWhitespace '\t' = true`, `isWhitespace '\n' = true`, etc. +- Model strings as `List Char` for easier structural reasoning +- Model `IEnumerable` as `List String` (finite; termination guaranteed since `pos` strictly increases each iteration) +- Exclude `Replace("\"", "")` subtlety from the state-machine model and handle it as a post-processing step +- The state machine can be modelled as a tail-recursive function with accumulator: `splitCL : String → State → String → List String` + +--- + +## Inferred Design Intent + +`SplitCommandLine` implements the minimal quoting semantics needed for response files: whitespace-delimited tokens with double-quote grouping, matching the upstream [dotnet/command-line-api](https://github.com/dotnet/command-line-api) implementation. It deliberately avoids backslash escaping to keep the tokeniser simple. The design favours simplicity over POSIX compliance. diff --git a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs index dc98be1809..65911e9565 100644 --- a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs +++ b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs @@ -12,6 +12,9 @@ internal sealed class BFSTestNodeVisitor { private static readonly string PathSeparatorString = TreeNodeFilter.PathSeparator.ToString(); + // Read-only; must never be mutated. Shared across all BFS traversals where ContainsPropertyFilters is false. + private static readonly PropertyBag EmptyPropertyBag = new(); + private readonly IEnumerable _rootTestNodes; private readonly ITestExecutionFilter _testExecutionFilter; private readonly TestArgumentsManager _testArgumentsManager; @@ -72,7 +75,10 @@ public async Task VisitAsync(Func onIncludedTestNo // When we are filtering as tree filter and the current node does not match the filter, we skip the node and its children. if (_testExecutionFilter is TreeNodeFilter treeNodeFilter) { - if (!treeNodeFilter.MatchesFilter(currentNodeFullPath, CreatePropertyBagForFilter(currentNode.Properties))) + PropertyBag filterPropertyBag = treeNodeFilter.ContainsPropertyFilters + ? new PropertyBag(currentNode.Properties) + : EmptyPropertyBag; + if (!treeNodeFilter.MatchesFilter(currentNodeFullPath, filterPropertyBag)) { continue; } @@ -100,17 +106,6 @@ public async Task VisitAsync(Func onIncludedTestNo DuplicatedNodes = [.. testNodesByUid.Where(x => x.Value.Count > 1)]; } - private static PropertyBag CreatePropertyBagForFilter(IProperty[] properties) - { - PropertyBag propertyBag = new(); - foreach (IProperty property in properties) - { - propertyBag.Add(property); - } - - return propertyBag; - } - private static string EncodeString(string value) => HttpUtility.UrlEncode(value); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs index 3f6c408568..7f93edbea5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs @@ -458,9 +458,9 @@ async Task DoRunAsync() { // Assembly initialize and class initialize logs are pre-pended to the first result. var testContextImpl = testContext as TestContextImplementation; - result.LogOutput = initializationLogs + testContextImpl?.GetOut(); - result.LogError = initializationErrorLogs + testContextImpl?.GetErr(); - result.DebugTrace = initializationTrace + testContextImpl?.GetTrace(); + result.LogOutput = initializationLogs + testContextImpl?.GetAndClearOutput(); + result.LogError = initializationErrorLogs + testContextImpl?.GetAndClearError(); + result.DebugTrace = initializationTrace + testContextImpl?.GetAndClearTrace(); result.TestContextMessages = initializationTestContextMessages + testContext.GetAndClearDiagnosticMessages(); } @@ -680,9 +680,9 @@ async Task DoRunAsync() Outcome = UnitTestOutcome.Failed, DisplayName = $"[{ClassType.FullName} ClassCleanup]", TestFailureException = ex, - LogOutput = testContextImpl?.GetOut(), - LogError = testContextImpl?.GetErr(), - DebugTrace = testContextImpl?.GetTrace(), + LogOutput = testContextImpl?.GetAndClearOutput(), + LogError = testContextImpl?.GetAndClearError(), + DebugTrace = testContextImpl?.GetAndClearTrace(), TestContextMessages = testContext.GetAndClearDiagnosticMessages(), }; } @@ -690,9 +690,9 @@ async Task DoRunAsync() if (results.Length > 0) { TestResult lastResult = results[results.Length - 1]; - lastResult.LogOutput += testContextImpl?.GetOut(); - lastResult.LogError += testContextImpl?.GetErr(); - lastResult.DebugTrace += testContextImpl?.GetTrace(); + lastResult.LogOutput += testContextImpl?.GetAndClearOutput(); + lastResult.LogError += testContextImpl?.GetAndClearError(); + lastResult.DebugTrace += testContextImpl?.GetAndClearTrace(); lastResult.TestContextMessages += testContext.GetAndClearDiagnosticMessages(); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs index ee240aab96..e45ed74e5f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs @@ -148,9 +148,9 @@ public virtual async Task InvokeAsync(object?[]? arguments) if (result != null) { var testContextImpl = TestContext as TestContextImplementation; - result.LogOutput = testContextImpl?.GetOut(); - result.LogError = testContextImpl?.GetErr(); - result.DebugTrace = testContextImpl?.GetTrace(); + result.LogOutput = testContextImpl?.GetAndClearOutput(); + result.LogError = testContextImpl?.GetAndClearError(); + result.DebugTrace = testContextImpl?.GetAndClearTrace(); result.TestContextMessages = TestContext?.GetAndClearDiagnosticMessages(); result.ResultFiles = TestContext?.GetResultFiles(); result.Duration = watch.Elapsed; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs index ab36a72657..8bee0a4a4d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs @@ -265,9 +265,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet finally { var testContextImpl = testContext.Context as TestContextImplementation; - result!.LogOutput = testContextImpl?.GetOut(); - result.LogError = testContextImpl?.GetErr(); - result.DebugTrace = testContextImpl?.GetTrace(); + result!.LogOutput = testContextImpl?.GetAndClearOutput(); + result.LogError = testContextImpl?.GetAndClearError(); + result.DebugTrace = testContextImpl?.GetAndClearTrace(); result.TestContextMessages = testContext.GetAndClearDiagnosticMessages(); } @@ -288,9 +288,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet { Outcome = UnitTestOutcome.Failed, TestFailureException = ex, - LogOutput = testContextImpl?.GetOut(), - LogError = testContextImpl?.GetErr(), - DebugTrace = testContextImpl?.GetTrace(), + LogOutput = testContextImpl?.GetAndClearOutput(), + LogError = testContextImpl?.GetAndClearError(), + DebugTrace = testContextImpl?.GetAndClearTrace(), TestContextMessages = testContext.GetAndClearDiagnosticMessages(), }; } @@ -298,9 +298,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet if (results.Length > 0) { TestResult lastResult = results[results.Length - 1]; - lastResult.LogOutput += testContextImpl?.GetOut(); - lastResult.LogError += testContextImpl?.GetErr(); - lastResult.DebugTrace += testContextImpl?.GetTrace(); + lastResult.LogOutput += testContextImpl?.GetAndClearOutput(); + lastResult.LogError += testContextImpl?.GetAndClearError(); + lastResult.DebugTrace += testContextImpl?.GetAndClearTrace(); lastResult.TestContextMessages += testContext.GetAndClearDiagnosticMessages(); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs index 78bce06425..c7460abf34 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs @@ -46,6 +46,15 @@ internal void AppendLine(string? value) internal void Clear() => _builder.Clear(); + [MethodImpl(MethodImplOptions.Synchronized)] + internal string GetAndClear() + { + string result = _builder.ToString(); + _builder.Clear(); + + return result; + } + [MethodImpl(MethodImplOptions.Synchronized)] public override string ToString() => _builder.ToString(); @@ -380,12 +389,12 @@ private SynchronizedStringBuilder GetTestContextMessagesStringBuilder() return _testContextMessageStringBuilder; } - internal string? GetOut() - => _stdOutStringBuilder?.ToString(); + internal string? GetAndClearOutput() + => _stdOutStringBuilder?.GetAndClear(); - internal string? GetErr() - => _stdErrStringBuilder?.ToString(); + internal string? GetAndClearError() + => _stdErrStringBuilder?.GetAndClear(); - internal string? GetTrace() - => _traceStringBuilder?.ToString(); + internal string? GetAndClearTrace() + => _traceStringBuilder?.GetAndClear(); } diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx index 0a93a4d9fd..1d11cc494a 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx @@ -213,6 +213,9 @@ Remove 'out' and 'ref' modifiers + + Remove duplicate 'DataRow' + Use MSTest 'Description' attribute instead diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs index f981a95149..a0f223ffb3 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; using MSTest.Analyzers.Helpers; @@ -94,15 +95,18 @@ private static async Task ReplaceWithMSTestDescriptionAttributeAsync(D return document; } - // Replace the System.ComponentModel.Description attribute name with the MSTest Description attribute name. - // Since the MSTest namespace (Microsoft.VisualStudio.TestTools.UnitTesting) is already in scope, - // we can use just the simple name "Description" which will resolve to MSTest's DescriptionAttribute. - AttributeSyntax newAttribute = systemDescriptionAttribute.WithName( - SyntaxFactory.IdentifierName("Description") - .WithTriviaFrom(systemDescriptionAttribute.Name)); + // Replace the System.ComponentModel.Description attribute name with the fully-qualified MSTest Description + // attribute name, annotated for simplification. The Simplifier will reduce it to the simple name if the + // MSTest namespace is already in scope and there is no ambiguity; otherwise it keeps the fully-qualified form. + NameSyntax msTestDescriptionName = SyntaxFactory.ParseName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDescriptionAttribute) + .WithTriviaFrom(systemDescriptionAttribute.Name) + .WithAdditionalAnnotations(Simplifier.Annotation); + + AttributeSyntax newAttribute = systemDescriptionAttribute.WithName(msTestDescriptionName); SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + Document updatedDocument = document.WithSyntaxRoot(root.ReplaceNode(systemDescriptionAttribute, newAttribute)); - return document.WithSyntaxRoot(root.ReplaceNode(systemDescriptionAttribute, newAttribute)); + return await Simplifier.ReduceAsync(updatedDocument, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/DuplicateDataRowFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/DuplicateDataRowFixer.cs new file mode 100644 index 0000000000..262e8950db --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/DuplicateDataRowFixer.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Composition; + +using Analyzer.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using MSTest.Analyzers.Helpers; + +namespace MSTest.Analyzers; + +/// +/// Code fixer for . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DuplicateDataRowFixer))] +[Shared] +public sealed class DuplicateDataRowFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DiagnosticIds.DuplicateDataRowRuleId); + + /// + public override FixAllProvider GetFixAllProvider() + // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + Diagnostic diagnostic = context.Diagnostics[0]; + + SyntaxNode diagnosticNode = root.FindNode(diagnostic.Location.SourceSpan); + AttributeSyntax? attributeSyntax = diagnosticNode.FirstAncestorOrSelf(); + if (attributeSyntax is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: CodeFixResources.RemoveDuplicateDataRowFix, + createChangedDocument: ct => RemoveDuplicateDataRowAsync(context.Document, attributeSyntax, ct), + equivalenceKey: nameof(DuplicateDataRowFixer)), + diagnostic); + } + + private static async Task RemoveDuplicateDataRowAsync(Document document, AttributeSyntax attributeSyntax, CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (attributeSyntax.Parent is not AttributeListSyntax attributeList) + { + return document; + } + + SyntaxNode newRoot; + if (attributeList.Attributes.Count == 1) + { + // Remove the entire attribute list if this is the only attribute in it + newRoot = root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.AddElasticMarker)!; + } + else + { + // Remove only this attribute from the list + AttributeListSyntax newAttributeList = attributeList.RemoveNode(attributeSyntax, SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.AddElasticMarker)!; + newRoot = root.ReplaceNode(attributeList, newAttributeList); + } + + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidCodeFix.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidFixer.cs similarity index 96% rename from src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidCodeFix.cs rename to src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidFixer.cs index 263cfe53e2..ee863595f7 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidCodeFix.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/TestMethodShouldBeValidFixer.cs @@ -19,11 +19,11 @@ namespace MSTest.Analyzers; /// -/// Code fix for . +/// Code fixer for . /// -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TestMethodShouldBeValidCodeFixProvider))] +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TestMethodShouldBeValidFixer))] [Shared] -public sealed class TestMethodShouldBeValidCodeFixProvider : CodeFixProvider +public sealed class TestMethodShouldBeValidFixer : CodeFixProvider { /// public override ImmutableArray FixableDiagnosticIds @@ -55,7 +55,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) CodeAction.Create( title: CodeFixResources.TestMethodShouldBeValidFix, createChangedSolution: c => FixTestMethodAsync(context.Document, methodDeclaration, c), - equivalenceKey: nameof(TestMethodShouldBeValidCodeFixProvider)), + equivalenceKey: nameof(TestMethodShouldBeValidFixer)), diagnostic); } diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf index b61998fbe8..722afbbd15 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf @@ -67,6 +67,11 @@ Odebrat argument ClassCleanupBehavior + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Pokud chcete použít výchozí AutoDetect, odeberte parametr DynamicDataSourceType. diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf index 9ad9d303f0..0a5a4b1fea 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf @@ -67,6 +67,11 @@ Entfernen Sie das Argument „ClassCleanupBehavior“. + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Entfernen Sie den Parameter „DynamicDataSourceType“, um die Standardeinstellung „AutoDetect“ zu verwenden. diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf index d708829e76..bd6aaeae95 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf @@ -67,6 +67,11 @@ Quitar el argumento "ClassCleanupBehavior" + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Elimine el parámetro 'DynamicDataSourceType' para utilizar el valor predeterminado 'AutoDetect' diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf index eb70dc03d5..3925d6c798 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf @@ -67,6 +67,11 @@ Supprimez l’argument « ClassCleanupBehavior » + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Pour utiliser la valeur par défaut « AutoDetect », supprimez le paramètre « DynamicDataSourceType » diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf index bf367d6d70..30882dc38e 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf @@ -67,6 +67,11 @@ Rimuovere l'argomento 'ClassCleanupBehavior' + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Rimuovi il parametro ''DynamicDataSourceType'' per utilizzare il valore predefinito ''AutoDetect'' diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf index dc3ef77325..a334f49ac1 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf @@ -67,6 +67,11 @@ 'ClassCleanupBehavior' 引数を削除する + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' 'DynamicDataSourceType' パラメーターを削除して、既定の 'AutoDetect' を使用します diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf index 467c2bf340..4da44421c0 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf @@ -67,6 +67,11 @@ 'ClassCleanupBehavior' 인수 제거 + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' 기본 'AutoDetect'를 사용하려면 'DynamicDataSourceType' 매개 변수를 제거합니다. diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf index a6cd22654f..cf68f15258 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf @@ -67,6 +67,11 @@ Usuń argument „ClassCleanupBehavior” + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Usuń parametr „DynamicDataSourceType”, aby użyć domyślnego elementu „AutoDetect” diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf index e580cbaee2..a293b3c41e 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf @@ -67,6 +67,11 @@ Remover o argumento "ClassCleanupBehavior" + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Remover o parâmetro "DynamicDataSourceType" para usar o padrão "AutoDetect" diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf index ded6db441d..47374accfd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf @@ -67,6 +67,11 @@ Удалить аргумент "ClassCleanupBehavior" + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Удалите параметр "DynamicDataSourceType", чтобы использовать значение по умолчанию "AutoDetect" diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf index 8188db380e..4774553d44 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf @@ -67,6 +67,11 @@ 'ClassCleanupBehavior' bağımsız değişkenini kaldırın + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' Varsayılan ‘AutoDetect’ değerini kullanmak için ‘DynamicDataSourceType’ parametresini kaldırın diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf index 48d95e5157..1464045a21 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf @@ -67,6 +67,11 @@ 删除参数‘ClassCleanupBehavior’ + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' 移除 ‘DynamicDataSourceType’ 参数以使用默认的 ‘AutoDetect’ diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf index e9347165e7..6c2e650c45 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf @@ -67,6 +67,11 @@ 移除 'ClassCleanupBehavior' 參數 + + Remove duplicate 'DataRow' + Remove duplicate 'DataRow' + + Remove 'DynamicDataSourceType' parameter to use default 'AutoDetect' 移除 'DynamicDataSourceType' 參數,以使用預設的 'AutoDetect' diff --git a/src/Analyzers/MSTest.Analyzers/TestContextPropertyUsageAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/TestContextPropertyUsageAnalyzer.cs index 1cedf89c78..d9c0b2473d 100644 --- a/src/Analyzers/MSTest.Analyzers/TestContextPropertyUsageAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/TestContextPropertyUsageAnalyzer.cs @@ -14,7 +14,7 @@ namespace MSTest.Analyzers; /// -/// MSTEST0047: . +/// MSTEST0048: . /// [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class TestContextPropertyUsageAnalyzer : DiagnosticAnalyzer diff --git a/src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs b/src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs index 3533f67fc6..18a53c689b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs +++ b/src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs @@ -298,16 +298,16 @@ public async Task OrchestrateTestHostExecutionAsync(CancellationToken cance catch (OperationCanceledException) when (processExitedCancellationToken.IsCancellationRequested) { await outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, ExtensionResources.TestHostProcessExitedBeforeRetryCouldConnect, testHostProcess.ExitCode)), cancellationToken).ConfigureAwait(false); - return ExitCodes.GenericFailure; + return (int)ExitCode.GenericFailure; } } await testHostProcess.WaitForExitAsync().ConfigureAwait(false); exitCodes.Add(testHostProcess.ExitCode); - if (testHostProcess.ExitCode != ExitCodes.Success) + if (testHostProcess.ExitCode != (int)ExitCode.Success) { - if (testHostProcess.ExitCode != ExitCodes.AtLeastOneTestFailed) + if (testHostProcess.ExitCode != (int)ExitCode.AtLeastOneTestFailed) { await outputDevice.DisplayAsync(this, new WarningMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, ExtensionResources.TestSuiteFailedWithWrongExitCode, testHostProcess.ExitCode)), cancellationToken).ConfigureAwait(false); retryInterrupted = true; @@ -370,7 +370,7 @@ public async Task OrchestrateTestHostExecutionAsync(CancellationToken cance if (!thresholdPolicyKickedIn && !retryInterrupted) { - if (exitCodes[^1] != ExitCodes.Success) + if (exitCodes[^1] != (int)ExitCode.Success) { await outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, ExtensionResources.TestSuiteFailedInAllAttempts, userMaxRetryCount + 1)), cancellationToken).ConfigureAwait(false); } diff --git a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxCompareTool.cs b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxCompareTool.cs index 5dbb643375..d5c3e33bfc 100644 --- a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxCompareTool.cs +++ b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxCompareTool.cs @@ -73,12 +73,12 @@ await _task.WhenAll( if (AreMatchingTrxFiles(baseLineResults, comparedResults, outputBuilder)) { await _outputDisplay.DisplayAsync(this, new TextOutputDeviceData(outputBuilder.ToString()), cancellationToken).ConfigureAwait(false); - return ExitCodes.Success; + return (int)ExitCode.Success; } else { await _outputDisplay.DisplayAsync(this, new TextOutputDeviceData(outputBuilder.ToString()), cancellationToken).ConfigureAwait(false); - return ExitCodes.GenericFailure; + return (int)ExitCode.GenericFailure; } } diff --git a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs index 61ebb27157..831e83767d 100644 --- a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs +++ b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs @@ -165,7 +165,7 @@ public TrxReportEngine( AddTestLists(testRun); bool hasFailedTests = summaryCounts.Failed > 0 || summaryCounts.Timedout > 0; - string trxOutcome = isTestHostCrashed || _exitCode != ExitCodes.Success || hasFailedTests ? "Failed" : "Completed"; + string trxOutcome = isTestHostCrashed || _exitCode != (int)ExitCode.Success || hasFailedTests ? "Failed" : "Completed"; AddResultSummary(testRun, trxOutcome, runDeploymentRoot, testHostCrashInfo, _exitCode, summaryCounts, isTestHostCrashed); @@ -333,7 +333,7 @@ private void AddResultSummary(XElement testRun, string resultSummaryOutcome, str runInfo.Add(text); runInfos.Add(runInfo); } - else if (exitCode != ExitCodes.Success) + else if (exitCode != (int)ExitCode.Success) { var runInfos = new XElement(NamespaceUri + "RunInfos"); resultSummary.Add(runInfos); diff --git a/src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/InvokeTestingPlatformTask.cs b/src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/InvokeTestingPlatformTask.cs index ba321f8e74..9119358872 100644 --- a/src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/InvokeTestingPlatformTask.cs +++ b/src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/InvokeTestingPlatformTask.cs @@ -433,14 +433,14 @@ protected override void LogToolCommand(string message) protected override bool HandleTaskExecutionErrors() { // This is an unexpected situation we simply print to the console the output and return false. - if (string.IsNullOrEmpty(_outputFileName) && ExitCode != ExitCodes.InvalidCommandLine) + if (string.IsNullOrEmpty(_outputFileName) && ExitCode != (int)Helpers.ExitCode.InvalidCommandLine) { Log.LogError(null, "run failed", null, TargetPath.ItemSpec.Trim(), 0, 0, 0, 0, Resources.MSBuildResources.TestFailedNoDetail, _output); } else { // If the output file name is null and the exit code is invalid command line we create a default one. - if (_outputFileName is null && ExitCode == ExitCodes.InvalidCommandLine) + if (_outputFileName is null && ExitCode == (int)Helpers.ExitCode.InvalidCommandLine) { _outputFileName = Path.Combine(Path.GetDirectoryName(TargetPath.ItemSpec.Trim())!, "TestResults"); _fileSystem.CreateDirectory(_outputFileName); diff --git a/src/Platform/Microsoft.Testing.Platform/Extensions/AbortForMaxFailedTestsExtension.cs b/src/Platform/Microsoft.Testing.Platform/Extensions/AbortForMaxFailedTestsExtension.cs index d7cad3db4a..63c2c2884c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Extensions/AbortForMaxFailedTestsExtension.cs +++ b/src/Platform/Microsoft.Testing.Platform/Extensions/AbortForMaxFailedTestsExtension.cs @@ -67,7 +67,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella return; } - if (TestNodePropertiesCategories.WellKnownTestNodeTestRunOutcomeFailedProperties.Any(t => t == testNodeStateProperty.GetType()) && + if (Array.IndexOf(TestNodePropertiesCategories.WellKnownTestNodeTestRunOutcomeFailedProperties, testNodeStateProperty.GetType()) != -1 && ++_failCount >= _maxFailedTests.Value && // If already triggered, don't do it again. !_policiesService.IsMaxFailedTestsTriggered) diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/ExitCodes.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/ExitCodes.cs index 070a33b6bf..6cb102d711 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/ExitCodes.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/ExitCodes.cs @@ -9,25 +9,21 @@ namespace Microsoft.Testing.Platform.Helpers; /// We use positive exit codes for failure because POSIX/BASH exit codes are unsigned 8-bit integers. /// On POSIX systems the standard exit code is 0 for success and any number from 1 to 255 for anything else. /// -// TODO: Consider changing this to an enum, and rename to 'ExitCode' to follow enum naming convention. -// Being an enum makes it easier to do 'Enum.IsDefined' checks to validate if an exit code is a known MTP exit code. -// Note: Changing this to enum would be binary breaking for extensions built against MTP <= 2.1 that still consume this via IVT -// (those extensions reference the class directly from the MTP assembly, not via source embedding). [Embedded] -internal static class ExitCodes +internal enum ExitCode { - public const int Success = 0; - public const int GenericFailure = 1; - public const int AtLeastOneTestFailed = 2; - public const int TestSessionAborted = 3; - public const int InvalidPlatformSetup = 4; - public const int InvalidCommandLine = 5; - // public const int FeatureNotImplemented = 6; - public const int TestHostProcessExitedNonGracefully = 7; - public const int ZeroTests = 8; - public const int MinimumExpectedTestsPolicyViolation = 9; - public const int TestAdapterTestSessionFailure = 10; - public const int DependentProcessExited = 11; - public const int IncompatibleProtocolVersion = 12; - public const int TestExecutionStoppedForMaxFailedTests = 13; + Success = 0, + GenericFailure = 1, + AtLeastOneTestFailed = 2, + TestSessionAborted = 3, + InvalidPlatformSetup = 4, + InvalidCommandLine = 5, + // FeatureNotImplemented = 6, + TestHostProcessExitedNonGracefully = 7, + ZeroTests = 8, + MinimumExpectedTestsPolicyViolation = 9, + TestAdapterTestSessionFailure = 10, + DependentProcessExited = 11, + IncompatibleProtocolVersion = 12, + TestExecutionStoppedForMaxFailedTests = 13, } diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/NonCooperativeParentProcessListener.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/NonCooperativeParentProcessListener.cs index 01f094bffe..ae627deb2b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/NonCooperativeParentProcessListener.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/NonCooperativeParentProcessListener.cs @@ -35,11 +35,11 @@ private void SubscribeToParentProcess() { // If we fail the process is already gone, so we can just exit. // The first check is already done inside the command line parser. - _environment.Exit(ExitCodes.DependentProcessExited); + _environment.Exit((int)ExitCode.DependentProcessExited); } } - private void ParentProcess_Exited(object? sender, EventArgs e) => _environment.Exit(ExitCodes.DependentProcessExited); + private void ParentProcess_Exited(object? sender, EventArgs e) => _environment.Exit((int)ExitCode.DependentProcessExited); public void Dispose() => _parentProcess?.Dispose(); } diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs index 6726d52e73..2ffa4618de 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs @@ -30,7 +30,7 @@ public async Task RunAsync() { CancellationToken testApplicationCancellationToken = ServiceProvider.GetTestApplicationCancellationTokenSource().CancellationToken; - int exitCode = ExitCodes.GenericFailure; + int exitCode = (int)ExitCode.GenericFailure; IPlatformOpenTelemetryService? platformOTelService = null; IPlatformActivity? activity = null; try @@ -45,7 +45,7 @@ public async Task RunAsync() if (testApplicationCancellationToken.IsCancellationRequested) { - exitCode = ExitCodes.TestSessionAborted; + exitCode = (int)ExitCode.TestSessionAborted; } return exitCode; @@ -59,7 +59,7 @@ public async Task RunAsync() exitCode = isValidProtocol ? await RunTestAppAsync(platformOTelService, testApplicationCancellationToken).ConfigureAwait(false) - : ExitCodes.IncompatibleProtocolVersion; + : (int)ExitCode.IncompatibleProtocolVersion; } finally { @@ -90,7 +90,7 @@ public async Task RunAsync() if (testApplicationCancellationToken.IsCancellationRequested) { - exitCode = ExitCodes.TestSessionAborted; + exitCode = (int)ExitCode.TestSessionAborted; } return exitCode; diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs index 4c52364b4e..0ff442bd35 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs @@ -108,7 +108,7 @@ await ExecuteRequestAsync( { requestExecuteStop ??= _clock.UtcNow; - exitCode = ExitCodes.TestSessionAborted; + exitCode = (int)ExitCode.TestSessionAborted; await _logger.LogInformationAsync("Test session canceled.").ConfigureAwait(false); } finally @@ -128,7 +128,7 @@ await ExecuteRequestAsync( { TelemetryProperties.RequestProperties.AdapterLoadStop, adapterLoadStop }, { TelemetryProperties.RequestProperties.RequestExecuteStart, requestExecuteStart }, { TelemetryProperties.RequestProperties.RequestExecuteStop, requestExecuteStop }, - { TelemetryProperties.HostProperties.ExitCodePropertyName, cancellationToken.IsCancellationRequested ? ExitCodes.TestSessionAborted : exitCode.ToString(CultureInfo.InvariantCulture) }, + { TelemetryProperties.HostProperties.ExitCodePropertyName, exitCode.ToString(CultureInfo.InvariantCulture) }, }; if (statistics is not null) diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs index 7731b8383e..3c8b6d80f4 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs @@ -170,8 +170,8 @@ or InvalidOperationException // If the global cancellation is called together with the server closing one the server exited gracefully. return !cancellationToken.IsCancellationRequested && _serverClosingTokenSource.IsCancellationRequested - ? ExitCodes.Success - : ExitCodes.TestSessionAborted; + ? (int)ExitCode.Success + : (int)ExitCode.TestSessionAborted; } /// diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index 466be943f2..ba972ca467 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -261,7 +261,7 @@ public async Task BuildAsync( builderActivity?.SetTag(BuilderHostTypeOTelKey, nameof(InformativeCommandLineHost)); builderActivity?.Dispose(); - return new InformativeCommandLineHost(ExitCodes.InvalidCommandLine, serviceProvider); + return new InformativeCommandLineHost((int)ExitCode.InvalidCommandLine, serviceProvider); } // Register as ICommandLineOptions. diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs index 0103e1fefc..b9cef2c7c3 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs @@ -221,7 +221,7 @@ protected override async Task InternalRunAsync(CancellationToken cancellati await outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(displayErrorMessageBuilder.ToString()), cancellationToken).ConfigureAwait(false); await _logger.LogErrorAsync(logErrorMessageBuilder.ToString()).ConfigureAwait(false); - return ExitCodes.InvalidPlatformSetup; + return (int)ExitCode.InvalidPlatformSetup; } foreach (EnvironmentVariable envVar in environmentVariables.GetAll()) @@ -346,17 +346,17 @@ protected override async Task InternalRunAsync(CancellationToken cancellati // If we have a process in the middle between the test host controller and the test host process we need to keep it into account. exitCode = testHostProcess.ExitCode; - if (exitCode == ExitCodes.Success && cancellationToken.IsCancellationRequested) + if (exitCode == (int)ExitCode.Success && cancellationToken.IsCancellationRequested) { // In case of cancellation, only alter exit code if it was success. // If there is another exit code indicating another failure, we prefer it over the cancellation. - exitCode = ExitCodes.TestSessionAborted; + exitCode = (int)ExitCode.TestSessionAborted; } else if (!testHostProcessInformation.HasExitedGracefully || _testHostExitCodeReceived != testHostProcess.ExitCode) { await outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, PlatformResources.TestProcessDidNotExitGracefullyErrorMessage, testHostProcess.ExitCode)), cancellationToken).ConfigureAwait(false); - exitCode = ExitCodes.TestHostProcessExitedNonGracefully; + exitCode = (int)ExitCode.TestHostProcessExitedNonGracefully; } await _logger.LogInformationAsync($"TestHostControllersTestHost ended with exit code '{exitCode}' (real test host exit code '{testHostProcess.ExitCode}') in '{consoleRunStarted.Elapsed}'").ConfigureAwait(false); diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostOchestratorHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostOchestratorHost.cs index 0b8dc8105b..ee2c567892 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostOchestratorHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostOchestratorHost.cs @@ -46,7 +46,7 @@ public async Task RunAsync() catch (OperationCanceledException) when (applicationCancellationToken.CancellationToken.IsCancellationRequested) { // We do nothing we're canceling - exitCode = ExitCodes.TestSessionAborted; + exitCode = (int)ExitCode.TestSessionAborted; } return exitCode; diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ToolsTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ToolsTestHost.cs index 1a664a7956..d5cc39b7db 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ToolsTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ToolsTestHost.cs @@ -63,20 +63,20 @@ public async Task RunAsync() { await _outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(unknownOptionsError), cancellationToken).ConfigureAwait(false); console.WriteLine(); - return ExitCodes.InvalidCommandLine; + return (int)ExitCode.InvalidCommandLine; } if (ExtensionArgumentArityAreInvalid(out string? arityErrors, tool)) { await _outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(arityErrors), cancellationToken).ConfigureAwait(false); - return ExitCodes.InvalidCommandLine; + return (int)ExitCode.InvalidCommandLine; } ValidationResult optionsArgumentsValidationResult = await ValidateOptionsArgumentsAsync(tool).ConfigureAwait(false); if (!optionsArgumentsValidationResult.IsValid) { await _outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData(optionsArgumentsValidationResult.ErrorMessage), cancellationToken).ConfigureAwait(false); - return ExitCodes.InvalidCommandLine; + return (int)ExitCode.InvalidCommandLine; } return await tool.RunAsync(cancellationToken).ConfigureAwait(false); @@ -85,7 +85,7 @@ public async Task RunAsync() await _outputDevice.DisplayAsync(this, new ErrorMessageOutputDeviceData($"Tool '{toolNameToRun}' not found in the list of registered tools."), cancellationToken).ConfigureAwait(false); await _commandLineHandler.PrintHelpAsync(_outputDevice, null, cancellationToken).ConfigureAwait(false); - return ExitCodes.InvalidCommandLine; + return (int)ExitCode.InvalidCommandLine; } private bool UnknownOptions([NotNullWhen(true)] out string? error, ITool tool) diff --git a/src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs b/src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs index 4f840d3235..86bdfa6183 100644 --- a/src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs +++ b/src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs @@ -181,7 +181,7 @@ public async Task RequestReplyAsync(TRequest req // This is especially important for 'dotnet test', where the user can simply kill the dotnet.exe process themselves. // In that case, we want the MTP process to also die. // Exit code 1 indicates abnormal termination due to IPC connection loss. - _environment.Exit(ExitCodes.GenericFailure); + _environment.Exit((int)ExitCode.GenericFailure); } // Reset the current chunk size diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs index 36f3a940d0..01c98c6b0f 100644 --- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs +++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs @@ -274,9 +274,38 @@ public TProperty[] OfType() } // We don't want to allocate an array if we know that we're looking for a TestNodeStateProperty - return typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)) + if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)) || _property is null) + { + return []; + } + + // Direct linked-list walk: avoids allocating a yield-iterator state machine + // (the original code called _property.OfType() which uses yield return). + TProperty? first = default; + bool foundAny = false; + List? overflow = null; + Property? current = _property; + while (current is not null) + { + if (current.Current is TProperty match) + { + if (!foundAny) + { + first = match; + foundAny = true; + } + else + { + (overflow ??= [first!]).Add(match); + } + } + + current = current.Next; + } + + return !foundAny ? [] - : _property is null ? [] : [.. _property.OfType()]; + : overflow is not null ? [.. overflow] : [first!]; } /// diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index 736ebbbd91..da7178a629 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -26,6 +26,7 @@ internal TreeNodeFilter(string filter) { Filter = filter ?? throw new ArgumentNullException(nameof(filter)); _filters = ParseFilter(filter); + ContainsPropertyFilters = _filters.Any(HasPropertyFilterExpression); } /// @@ -33,6 +34,13 @@ internal TreeNodeFilter(string filter) /// public string Filter { get; } + /// + /// Gets a value indicating whether any filter segment contains a property expression (e.g., Method[Trait=Foo]). + /// When , the argument to is never + /// inspected, and callers may safely pass an empty bag to avoid per-node allocation. + /// + internal bool ContainsPropertyFilters { get; } + /// /// The current grammar for the filter looks as follows: /// @@ -578,4 +586,8 @@ private static bool IsMatchingProperty(IProperty prop, ValueExpression propExpr, => prop is TestMetadataProperty testMetadataProperty && propExpr.Regex.IsMatch(testMetadataProperty.Key) && valueExpr.Regex.IsMatch(testMetadataProperty.Value); + + private static bool HasPropertyFilterExpression(FilterExpression expression) + => expression is ValueAndPropertyExpression || + (expression is OperatorExpression op && op.SubExpressions.Any(HasPropertyFilterExpression)); } diff --git a/src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs b/src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs index 80dbd77cc6..d136a4de87 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs @@ -129,16 +129,16 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo public int GetProcessExitCode() { - int exitCode = ExitCodes.Success; - exitCode = exitCode == ExitCodes.Success && _policiesService.IsMaxFailedTestsTriggered ? ExitCodes.TestExecutionStoppedForMaxFailedTests : exitCode; - exitCode = exitCode == ExitCodes.Success && _testAdapterTestSessionFailure ? ExitCodes.TestAdapterTestSessionFailure : exitCode; - exitCode = exitCode == ExitCodes.Success && _failedTestsCount > 0 ? ExitCodes.AtLeastOneTestFailed : exitCode; - exitCode = exitCode == ExitCodes.Success && _policiesService.IsAbortTriggered ? ExitCodes.TestSessionAborted : exitCode; - exitCode = exitCode == ExitCodes.Success && _totalRanTests == 0 ? ExitCodes.ZeroTests : exitCode; + ExitCode exitCode = ExitCode.Success; + exitCode = exitCode == ExitCode.Success && _policiesService.IsMaxFailedTestsTriggered ? ExitCode.TestExecutionStoppedForMaxFailedTests : exitCode; + exitCode = exitCode == ExitCode.Success && _testAdapterTestSessionFailure ? ExitCode.TestAdapterTestSessionFailure : exitCode; + exitCode = exitCode == ExitCode.Success && _failedTestsCount > 0 ? ExitCode.AtLeastOneTestFailed : exitCode; + exitCode = exitCode == ExitCode.Success && _policiesService.IsAbortTriggered ? ExitCode.TestSessionAborted : exitCode; + exitCode = exitCode == ExitCode.Success && _totalRanTests == 0 ? ExitCode.ZeroTests : exitCode; if (_commandLineOptions.TryGetOptionArgumentList(PlatformCommandLineProvider.MinimumExpectedTestsOptionKey, out string[]? argumentList)) { - exitCode = exitCode == ExitCodes.Success && _totalRanTests < int.Parse(argumentList[0], CultureInfo.InvariantCulture) ? ExitCodes.MinimumExpectedTestsPolicyViolation : exitCode; + exitCode = exitCode == ExitCode.Success && _totalRanTests < int.Parse(argumentList[0], CultureInfo.InvariantCulture) ? ExitCode.MinimumExpectedTestsPolicyViolation : exitCode; } // If the user has specified the IgnoreExitCode, then we don't want to return a non-zero exit code if the exit code matches the one specified. @@ -153,13 +153,13 @@ public int GetProcessExitCode() if (exitCodeToIgnore is not null) { - if (exitCodeToIgnore.Split(';').Any(code => int.TryParse(code, out int parsedExitCode) && parsedExitCode == exitCode)) + if (exitCodeToIgnore.Split(';').Any(code => int.TryParse(code, out int parsedExitCode) && parsedExitCode == (int)exitCode)) { - exitCode = ExitCodes.Success; + exitCode = ExitCode.Success; } } - return exitCode; + return (int)exitCode; } public async Task SetTestAdapterTestSessionFailureAsync(string errorMessage, CancellationToken cancellationToken) diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equality.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equality.cs new file mode 100644 index 0000000000..3ace0a74dc --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equality.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region AreEqual + + /// + /// Tests whether the specified collections are equal and throws an exception + /// if the two collections are not equal. Equality is defined as having the same + /// elements in the same order and quantity. Whether two elements are the same + /// is checked using method. + /// Different references to the same value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects. + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// Thrown if is not equal to + /// . + /// + public static void AreEqual(ICollection? expected, ICollection? actual) + => AreEqual(expected, actual, string.Empty); + + /// + /// Tests whether the specified collections are equal and throws an exception + /// if the two collections are not equal. Equality is defined as having the same + /// elements in the same order and quantity. Whether two elements are the same + /// is checked using method. + /// Different references to the same value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects. + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The message to include in the exception when + /// is not equal to . The message is shown in + /// test results. + /// + /// + /// Thrown if is not equal to + /// . + /// + public static void AreEqual(ICollection? expected, ICollection? actual, string? message) + { + string reason = string.Empty; + if (!AreCollectionsEqual(expected, actual, new ObjectComparer(), ref reason)) + { + string finalMessage = ConstructFinalMessage(reason, message); + Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); + } + } + + /// + /// Tests whether the specified collections are unequal and throws an exception + /// if the two collections are equal. Equality is defined as having the same + /// elements in the same order and quantity. Whether two elements are the same + /// is checked using method. + /// Different references to the same value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects + /// not to match . + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// Thrown if is equal to . + /// + public static void AreNotEqual(ICollection? notExpected, ICollection? actual) + => AreNotEqual(notExpected, actual, string.Empty); + + /// + /// Tests whether the specified collections are unequal and throws an exception + /// if the two collections are equal. Equality is defined as having the same + /// elements in the same order and quantity. Whether two elements are the same + /// is checked using method. + /// Different references to the same value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects + /// not to match . + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The message to include in the exception when + /// is equal to . The message is shown in + /// test results. + /// + /// + /// Thrown if is equal to . + /// + public static void AreNotEqual(ICollection? notExpected, ICollection? actual, string? message) + { + string reason = string.Empty; + if (AreCollectionsEqual(notExpected, actual, new ObjectComparer(), ref reason)) + { + string finalMessage = ConstructFinalMessage(reason, message); + Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + } + } + + /// + /// Tests whether the specified collections are equal and throws an exception + /// if the two collections are not equal. Equality is defined as having the same + /// elements in the same order and quantity. Different references to the same + /// value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects. + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// Thrown if is not equal to + /// . + /// + public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer) + => AreEqual(expected, actual, comparer, string.Empty); + + /// + /// Tests whether the specified collections are equal and throws an exception + /// if the two collections are not equal. Equality is defined as having the same + /// elements in the same order and quantity. Different references to the same + /// value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects. + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// The message to include in the exception when + /// is not equal to . The message is shown in + /// test results. + /// + /// + /// Thrown if is not equal to + /// . + /// + public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer, string? message) + { + string reason = string.Empty; + if (!AreCollectionsEqual(expected, actual, comparer, ref reason)) + { + string finalMessage = ConstructFinalMessage(reason, message); + Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); + } + } + + /// + /// Tests whether the specified collections are unequal and throws an exception + /// if the two collections are equal. Equality is defined as having the same + /// elements in the same order and quantity. Different references to the same + /// value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects + /// not to match . + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// Thrown if is equal to . + /// + public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [NotNull] IComparer? comparer) + => AreNotEqual(notExpected, actual, comparer, string.Empty); + + /// + /// Tests whether the specified collections are unequal and throws an exception + /// if the two collections are equal. Equality is defined as having the same + /// elements in the same order and quantity. Different references to the same + /// value are considered equal. + /// + /// + /// The first collection to compare. This is the collection the tests expects + /// not to match . + /// + /// + /// The second collection to compare. This is the collection produced by the + /// code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// The message to include in the exception when + /// is equal to . The message is shown in + /// test results. + /// + /// + /// Thrown if is equal to . + /// + public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [NotNull] IComparer? comparer, string? message) + { + string reason = string.Empty; + if (AreCollectionsEqual(notExpected, actual, comparer, ref reason)) + { + string finalMessage = ConstructFinalMessage(reason, message); + Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + } + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equivalence.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equivalence.cs new file mode 100644 index 0000000000..022604dd90 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Equivalence.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region Equivalence + + /// + /// Tests whether two collections contain the same elements and throws an + /// exception if either collection contains an element not in the other + /// collection. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// and nullabilities don't match, + /// or if any element was found in one of the collections but not the other. + /// + public static void AreEquivalent( + [NotNullIfNotNull(nameof(actual))] ICollection? expected, [NotNullIfNotNull(nameof(expected))] ICollection? actual) + => AreEquivalent(expected?.Cast(), actual?.Cast(), EqualityComparer.Default, string.Empty); + + /// + /// Tests whether two collections contain the same elements and throws an + /// exception if either collection contains an element not in the other + /// collection. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The message to include in the exception when an element was found + /// in one of the collections but not the other. The message is shown + /// in test results. + /// + /// + /// and nullabilities don't match, + /// or if any element was found in one of the collections but not the other. + /// + public static void AreEquivalent( + [NotNullIfNotNull(nameof(actual))] ICollection? expected, [NotNullIfNotNull(nameof(expected))] ICollection? actual, string? message) + => AreEquivalent(expected?.Cast(), actual?.Cast(), EqualityComparer.Default, message); + + /// + /// Tests whether two collections contain the same elements and throws an + /// exception if either collection contains an element not in the other + /// collection. + /// + /// + /// The type of values to compare. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// and nullabilities don't match, + /// or if any element was found in one of the collections but not the other. + /// + public static void AreEquivalent( + [NotNullIfNotNull(nameof(actual))] IEnumerable? expected, [NotNullIfNotNull(nameof(expected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer) + => AreEquivalent(expected, actual, comparer, string.Empty); + + /// + /// Tests whether two collections contain the same elements and throws an + /// exception if either collection contains an element not in the other + /// collection. + /// + /// + /// The type of values to compare. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// The message to include in the exception when an element was found + /// in one of the collections but not the other. The message is shown + /// in test results. + /// + /// + /// and nullabilities don't match, + /// or if any element was found in one of the collections but not the other. + /// + public static void AreEquivalent( + [NotNullIfNotNull(nameof(actual))] IEnumerable? expected, [NotNullIfNotNull(nameof(expected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer, + string? message) + { + Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); + + // Check whether one is null while the other is not. + if (expected == null != (actual == null)) + { + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); + } + + // If the references are the same or both collections are null, they are equivalent. + if (object.ReferenceEquals(expected, actual) || expected == null) + { + return; + } + + DebugEx.Assert(actual is not null, "actual is not null here"); + + int expectedCollectionCount = expected.Count(); + int actualCollectionCount = actual.Count(); + + // Check whether the element counts are different. + if (expectedCollectionCount != actualCollectionCount) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.ElementNumbersDontMatch, + userMessage, + expectedCollectionCount, + actualCollectionCount); + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + } + + // If both collections are empty, they are equivalent. + if (expectedCollectionCount == 0) + { + return; + } + + // Search for a mismatched element. + if (FindMismatchedElement(expected, actual, comparer, out int expectedCount, out int actualCount, out object? mismatchedElement)) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.ActualHasMismatchedElements, + userMessage, + expectedCount.ToString(CultureInfo.CurrentCulture.NumberFormat), + Assert.ReplaceNulls(mismatchedElement), + actualCount.ToString(CultureInfo.CurrentCulture.NumberFormat)); + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + } + + // All the elements and counts matched. + } + + /// + /// Tests whether two collections contain the different elements and throws an + /// exception if the two collections contain identical elements without regard + /// to order. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects to be different than the actual collection. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// and nullabilities don't match, + /// or if collections contain the same elements, including the same number of duplicate + /// occurrences of each element. + /// + public static void AreNotEquivalent( + [NotNullIfNotNull(nameof(actual))] ICollection? notExpected, [NotNullIfNotNull(nameof(notExpected))] ICollection? actual) + => AreNotEquivalent(notExpected?.Cast(), actual?.Cast(), EqualityComparer.Default, string.Empty); + + /// + /// Tests whether two collections contain the different elements and throws an + /// exception if the two collections contain identical elements without regard + /// to order. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects to be different than the actual collection. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The message to include in the exception when + /// contains the same elements as . The message + /// is shown in test results. + /// + /// + /// and nullabilities don't match, + /// or if collections contain the same elements, including the same number of duplicate + /// occurrences of each element. + /// + public static void AreNotEquivalent( + [NotNullIfNotNull(nameof(actual))] ICollection? notExpected, [NotNullIfNotNull(nameof(notExpected))] ICollection? actual, + string? message) + => AreNotEquivalent(notExpected?.Cast(), actual?.Cast(), comparer: EqualityComparer.Default, message); + + /// + /// Tests whether two collections contain the different elements and throws an + /// exception if the two collections contain identical elements without regard + /// to order. + /// + /// + /// The type of values to compare. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects to be different than the actual collection. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// and nullabilities don't match, + /// or if collections contain the same elements, including the same number of duplicate + /// occurrences of each element. + /// + public static void AreNotEquivalent( + [NotNullIfNotNull(nameof(actual))] IEnumerable? notExpected, [NotNullIfNotNull(nameof(notExpected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer) + => AreNotEquivalent(notExpected, actual, comparer, string.Empty); + + /// + /// Tests whether two collections contain the different elements and throws an + /// exception if the two collections contain identical elements without regard + /// to order. + /// + /// + /// The type of values to compare. + /// + /// + /// The first collection to compare. This contains the elements the test + /// expects to be different than the actual collection. + /// + /// + /// The second collection to compare. This is the collection produced by + /// the code under test. + /// + /// + /// The compare implementation to use when comparing elements of the collection. + /// + /// + /// The message to include in the exception when + /// contains the same elements as . The message + /// is shown in test results. + /// + /// + /// and nullabilities don't match, + /// or if collections contain the same elements, including the same number of duplicate + /// occurrences of each element. + /// + public static void AreNotEquivalent( + [NotNullIfNotNull(nameof(actual))] IEnumerable? notExpected, [NotNullIfNotNull(nameof(notExpected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer, + string? message) + { + Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); + + // Check whether one is null while the other is not. + if (notExpected == null != (actual == null)) + { + return; + } + + // If the references are the same or both collections are null, they + // are equivalent. object.ReferenceEquals will handle case where both are null. + if (object.ReferenceEquals(notExpected, actual)) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.BothCollectionsSameReference, + userMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + } + + DebugEx.Assert(actual is not null, "actual is not null here"); + DebugEx.Assert(notExpected is not null, "expected is not null here"); + + // Check whether the element counts are different. + int notExpectedCount = notExpected.Count(); + int actualCount = actual.Count(); + if (notExpectedCount != actualCount) + { + return; + } + + // If both collections are empty, they are equivalent. + if (notExpectedCount == 0) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.BothCollectionsEmpty, + userMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + } + + // Search for a mismatched element. + if (!FindMismatchedElement(notExpected, actual, comparer, out _, out _, out _)) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.BothSameElements, + userMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + } + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Helpers.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Helpers.cs new file mode 100644 index 0000000000..b23f306c6e --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Helpers.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region Helpers + + /// + /// Determines whether the first collection is a subset of the second + /// collection. If either set contains duplicate elements, the number + /// of occurrences of the element in the subset must be less than or + /// equal to the number of occurrences in the superset. + /// + /// + /// The collection the test expects to be contained in . + /// + /// + /// The collection the test expects to contain . + /// + /// + /// True if is a subset of + /// , false otherwise. + /// + internal static Tuple> IsSubsetOfHelper(ICollection subset, ICollection superset) + { + // $ CONSIDER: The current algorithm counts the number of occurrences of each + // $ CONSIDER: element in each collection and then compares the count, resulting + // $ CONSIDER: in an algorithm of ~n*log(n) + m*log(m) + n*log(m). It should be + // $ CONSIDER: faster to sort both collections and do an element-by-element + // $ CONSIDER: comparison, which should result in ~n*log(n) + m*log(m) + n. + var nonSubsetValues = new List(); + + // Count the occurrences of each object in both collections. + Dictionary subsetElements = GetElementCounts(subset.Cast(), EqualityComparer.Default, out int subsetNulls); + Dictionary supersetElements = GetElementCounts(superset.Cast(), EqualityComparer.Default, out int supersetNulls); + + bool isSubset = true; + + // Check null counts first + if (subsetNulls > supersetNulls) + { + isSubset = false; + // Add the excess null values to non-subset collection + for (int i = 0; i < (subsetNulls - supersetNulls); i++) + { + nonSubsetValues.Add(null); + } + } + + // Compare the counts of each object in the subset to the count of that object + // in the superset. + foreach (object? element in subsetElements.Keys) + { + subsetElements.TryGetValue(element, out int subsetCount); + supersetElements.TryGetValue(element, out int supersetCount); + + if (subsetCount > supersetCount) + { + isSubset = false; + // Add the excess occurrences to non-subset collection + int excessCount = subsetCount - supersetCount; + for (int i = 0; i < excessCount; i++) + { + nonSubsetValues.Add(element); + } + } + } + + return new Tuple>(isSubset, nonSubsetValues); + } + +#pragma warning disable CS8714 + /// + /// Constructs a dictionary containing the number of occurrences of each + /// element in the specified collection. + /// + /// + /// The collection to process. + /// + /// The equality comparer to use when comparing items. + /// + /// The number of null elements in the collection. + /// + /// + /// A dictionary containing the number of occurrences of each element + /// in the specified collection. + /// + private static Dictionary GetElementCounts(IEnumerable collection, IEqualityComparer comparer, out int nullCount) + { + DebugEx.Assert(collection != null, "Collection is Null."); + + var elementCounts = new Dictionary(comparer); + nullCount = 0; + + foreach (T? element in collection) + { + if (element == null) + { + nullCount++; + continue; + } + + elementCounts.TryGetValue(element, out int value); + value++; + elementCounts[element] = value; + } + + return elementCounts; + } + + /// + /// Finds a mismatched element between the two collections. A mismatched + /// element is one that appears a different number of times in the + /// expected collection than it does in the actual collection. The + /// collections are assumed to be different non-null references with the + /// same number of elements. The caller is responsible for this level of + /// verification. If there is no mismatched element, the function returns + /// false and the out parameters should not be used. + /// + /// + /// The first collection to compare. + /// + /// + /// The second collection to compare. + /// + /// The equality comparer to use when comparing items. + /// + /// The expected number of occurrences of + /// or 0 if there is no mismatched + /// element. + /// + /// + /// The actual number of occurrences of + /// or 0 if there is no mismatched + /// element. + /// + /// + /// The mismatched element (may be null) or null if there is no + /// mismatched element. + /// + /// + /// true if a mismatched element was found; false otherwise. + /// + private static bool FindMismatchedElement(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer, out int expectedCount, + out int actualCount, out object? mismatchedElement) + { + // $ CONSIDER: The current algorithm counts the number of occurrences of each + // $ CONSIDER: element in each collection and then compares the count, resulting + // $ CONSIDER: in an algorithm of ~n*log(n) + m*log(m) + n*log(m). It should be + // $ CONSIDER: faster to sort both collections and do an element-by-element + // $ CONSIDER: comparison, which should result in ~n*log(n) + m*log(m) + n. + + // Count the occurrences of each object in the both collections + Dictionary expectedElements = GetElementCounts(expected, comparer, out int expectedNulls); + Dictionary actualElements = GetElementCounts(actual, comparer, out int actualNulls); + + if (actualNulls != expectedNulls) + { + expectedCount = expectedNulls; + actualCount = actualNulls; + mismatchedElement = null; + return true; + } + + // Compare the counts of each object. Note that this comparison only needs + // to be done one way since comparing the total count is a prerequisite to + // calling this function. + foreach (T current in expectedElements.Keys) + { + expectedElements.TryGetValue(current, out expectedCount); + actualElements.TryGetValue(current, out actualCount); + + if (expectedCount != actualCount) + { + mismatchedElement = current; + return true; + } + } + + // All the elements and counts matched. + expectedCount = 0; + actualCount = 0; + mismatchedElement = null; + return false; + } +#pragma warning restore CS8714 + + private static bool AreCollectionsEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer, + ref string reason) + { + Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); + if (object.ReferenceEquals(expected, actual)) + { + reason = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsSameReference, string.Empty); + return true; + } + + return CompareIEnumerable(expected, actual, comparer, ref reason); + } + + private static bool CompareIEnumerable(IEnumerable? expected, IEnumerable? actual, IComparer comparer, ref string reason) + { + if ((expected == null) || (actual == null)) + { + return false; + } + + var stack = new Stack>(); + stack.Push(new(expected.GetEnumerator(), actual.GetEnumerator(), 0)); + + while (stack.Count > 0) + { + Tuple cur = stack.Pop(); + IEnumerator expectedEnum = cur.Item1; + IEnumerator actualEnum = cur.Item2; + int position = cur.Item3; + + while (expectedEnum.MoveNext()) + { + if (!actualEnum.MoveNext()) + { + reason = FrameworkMessages.NumberOfElementsDiff; + return false; + } + + object? curExpected = expectedEnum.Current; + object? curActual = actualEnum.Current; + if (comparer.Compare(curExpected, curActual) == 0) + { + position++; + } + else if (curExpected is IEnumerable curExpectedEnum && curActual is IEnumerable curActualEnum) + { + stack.Push(new(expectedEnum, actualEnum, position + 1)); + stack.Push(new(curExpectedEnum.GetEnumerator(), curActualEnum.GetEnumerator(), 0)); + } + else + { + reason = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.ElementsAtIndexDontMatch, + position, + Assert.ReplaceNulls(curExpected), + Assert.ReplaceNulls(curActual)); + return false; + } + } + + if (actualEnum.MoveNext() && !expectedEnum.MoveNext()) + { + reason = FrameworkMessages.NumberOfElementsDiff; + return false; + } + } + + reason = FrameworkMessages.BothCollectionsSameElements; + return true; + } + + private static string ConstructFinalMessage( + string reason, + string? message) + { + string userMessage = Assert.BuildUserMessage(message); + return userMessage.Length == 0 + ? reason + : string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CollectionEqualReason, userMessage, reason); + } + + /// + /// compares the objects using object.Equals. + /// + private sealed class ObjectComparer : IComparer + { + int IComparer.Compare(object? x, object? y) => Equals(x, y) ? 0 : -1; + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Membership.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Membership.cs new file mode 100644 index 0000000000..189ffadae1 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Membership.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region Membership + + /// + /// Tests whether the specified collection contains the specified element + /// and throws an exception if the element is not in the collection. + /// + /// + /// The collection in which to search for the element. + /// + /// + /// The element that is expected to be in the collection. + /// + /// + /// is null, or does not contain + /// element . + /// + public static void Contains([NotNull] ICollection? collection, object? element) + => Contains(collection, element, string.Empty); + + /// + /// Tests whether the specified collection contains the specified element + /// and throws an exception if the element is not in the collection. + /// + /// + /// The collection in which to search for the element. + /// + /// + /// The element that is expected to be in the collection. + /// + /// + /// The message to include in the exception when + /// is not in . The message is shown in + /// test results. + /// + /// + /// is null, or does not contain + /// element . + /// + public static void Contains([NotNull] ICollection? collection, object? element, string? message) + { + Assert.CheckParameterNotNull(collection, "CollectionAssert.Contains", "collection"); + + foreach (object? current in collection) + { + if (object.Equals(current, element)) + { + return; + } + } + + Assert.ReportAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); + } + + /// + /// Tests whether the specified collection does not contain the specified + /// element and throws an exception if the element is in the collection. + /// + /// + /// The collection in which to search for the element. + /// + /// + /// The element that is expected not to be in the collection. + /// + /// + /// is null, or contains + /// element . + /// + public static void DoesNotContain([NotNull] ICollection? collection, object? element) + => DoesNotContain(collection, element, string.Empty); + + /// + /// Tests whether the specified collection does not contain the specified + /// element and throws an exception if the element is in the collection. + /// + /// + /// The collection in which to search for the element. + /// + /// + /// The element that is expected not to be in the collection. + /// + /// + /// The message to include in the exception when + /// is in . The message is shown in test + /// results. + /// + /// + /// is null, or contains + /// element . + /// + public static void DoesNotContain([NotNull] ICollection? collection, object? element, string? message) + { + Assert.CheckParameterNotNull(collection, "CollectionAssert.DoesNotContain", "collection"); + + foreach (object? current in collection) + { + if (object.Equals(current, element)) + { + Assert.ReportAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); + } + } + } + + /// + /// Tests whether all items in the specified collection are non-null and throws + /// an exception if any element is null. + /// + /// + /// The collection in which to search for null elements. + /// + /// + /// is null, or contains a null element. + /// + public static void AllItemsAreNotNull([NotNull] ICollection? collection) + => AllItemsAreNotNull(collection, string.Empty); + + /// + /// Tests whether all items in the specified collection are non-null and throws + /// an exception if any element is null. + /// + /// + /// The collection in which to search for null elements. + /// + /// + /// The message to include in the exception when + /// contains a null element. The message is shown in test results. + /// + /// + /// is null, or contains a null element. + /// + public static void AllItemsAreNotNull([NotNull] ICollection? collection, string? message) + { + Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreNotNull", "collection"); + foreach (object? current in collection) + { + if (current == null) + { + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); + } + } + } + + /// + /// Tests whether all items in the specified collection are unique or not and + /// throws if any two elements in the collection are equal. + /// + /// + /// The collection in which to search for duplicate elements. + /// + /// + /// is null, or contains at least one duplicate + /// element. + /// + public static void AllItemsAreUnique([NotNull] ICollection? collection) + => AllItemsAreUnique(collection, string.Empty); + + /// + /// Tests whether all items in the specified collection are unique or not and + /// throws if any two elements in the collection are equal. + /// + /// + /// The collection in which to search for duplicate elements. + /// + /// + /// The message to include in the exception when + /// contains at least one duplicate element. The message is shown in + /// test results. + /// + /// + /// is null, or contains at least one duplicate + /// element. + /// + public static void AllItemsAreUnique([NotNull] ICollection? collection, string? message) + { + Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreUnique", "collection"); + + message = Assert.ReplaceNulls(message); + + bool foundNull = false; + HashSet table = []; + foreach (object? current in collection) + { + if (current == null) + { + if (!foundNull) + { + foundNull = true; + } + else + { + // Found a second occurrence of null. + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.AllItemsAreUniqueFailMsg, + userMessage, + FrameworkMessages.Common_NullInMessages); + + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + } + } + else + { + if (!table.Add(current)) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.AllItemsAreUniqueFailMsg, + userMessage, + Assert.ReplaceNulls(current)); + + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + } + } + } + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Subset.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Subset.cs new file mode 100644 index 0000000000..7d5fff305b --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Subset.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region Subset + + /// + /// Tests whether one collection is a subset of another collection and + /// throws an exception if any element in the subset is not also in the + /// superset. + /// + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// is null, or is null, + /// or contains at least one element not contained in + /// . + /// + public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset) + => IsSubsetOf(subset, superset, string.Empty); + + /// + /// Tests whether one collection is a subset of another collection and + /// throws an exception if any element in the subset is not also in the + /// superset. + /// + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// The message to include in the exception when an element in + /// is not found in . + /// The message is shown in test results. + /// + /// + /// is null, or is null, + /// or contains at least one element not contained in + /// . + /// + public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset, string? message) + { + Assert.CheckParameterNotNull(subset, "CollectionAssert.IsSubsetOf", "subset"); + Assert.CheckParameterNotNull(superset, "CollectionAssert.IsSubsetOf", "superset"); + Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); + if (!isSubsetValue.Item1) + { + string returnedSubsetValueMessage = string.Join(", ", isSubsetValue.Item2.Select(item => Convert.ToString(item, CultureInfo.InvariantCulture))); + + returnedSubsetValueMessage = string.Format(CultureInfo.InvariantCulture, FrameworkMessages.ReturnedSubsetValueMessage, returnedSubsetValueMessage); + string userMessage = Assert.BuildUserMessage(message); + if (string.IsNullOrEmpty(userMessage)) + { + Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); + } + else + { + Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); + } + } + } + + /// + /// Tests whether one collection is not a subset of another collection and + /// throws an exception if all elements in the subset are also in the + /// superset. + /// + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// is null, or is null, + /// or all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset) + => IsNotSubsetOf(subset, superset, string.Empty); + + /// + /// Tests whether one collection is not a subset of another collection and + /// throws an exception if all elements in the subset are also in the + /// superset. + /// + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// The message to include in the exception when every element in + /// is also found in . + /// The message is shown in test results. + /// + /// + /// is null, or is null, + /// or all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset, string? message) + { + Assert.CheckParameterNotNull(subset, "CollectionAssert.IsNotSubsetOf", "subset"); + Assert.CheckParameterNotNull(superset, "CollectionAssert.IsNotSubsetOf", "superset"); + Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); + if (isSubsetValue.Item1) + { + Assert.ReportAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); + } + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.Type.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Type.cs new file mode 100644 index 0000000000..eeae04b966 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.Type.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions associated +/// with collections within unit tests. If the condition being tested is not +/// met, an exception is thrown. +/// +public sealed partial class CollectionAssert +{ + #region Type + + /// + /// Tests whether all elements in the specified collection are instances + /// of the expected type and throws an exception if the expected type is + /// not in the inheritance hierarchy of one or more of the elements. + /// + /// + /// The collection containing elements the test expects to be of the + /// specified type. + /// + /// + /// The expected type of each element of . + /// + /// + /// is null or, is null, + /// or some elements of do not inherit/implement + /// . + /// + public static void AllItemsAreInstancesOfType([NotNull] ICollection? collection, [NotNull] Type? expectedType) + => AllItemsAreInstancesOfType(collection, expectedType, string.Empty); + + /// + /// Tests whether all elements in the specified collection are instances + /// of the expected type and throws an exception if the expected type is + /// not in the inheritance hierarchy of one or more of the elements. + /// + /// + /// The collection containing elements the test expects to be of the + /// specified type. + /// + /// + /// The expected type of each element of . + /// + /// + /// The message to include in the exception when an element in + /// is not an instance of + /// . The message is shown in test results. + /// + /// + /// is null or, is null, + /// or some elements of do not inherit/implement + /// . + /// + public static void AllItemsAreInstancesOfType( + [NotNull] ICollection? collection, [NotNull] Type? expectedType, string? message) + { + Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreInstancesOfType", "collection"); + Assert.CheckParameterNotNull(expectedType, "CollectionAssert.AllItemsAreInstancesOfType", "expectedType"); + int i = 0; + foreach (object? element in collection) + { + if (element?.GetType() is { } elementType + && !expectedType.IsAssignableFrom(elementType)) + { + string userMessage = Assert.BuildUserMessage(message); + string finalMessage = string.Format( + CultureInfo.CurrentCulture, + FrameworkMessages.ElementTypesAtIndexDontMatch, + userMessage, + i, + expectedType.ToString(), + element.GetType().ToString()); + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); + } + + i++; + } + } + + #endregion +} diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs index a09ba33955..2bfa764a3e 100644 --- a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs @@ -8,7 +8,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// with collections within unit tests. If the condition being tested is not /// met, an exception is thrown. /// -public sealed class CollectionAssert +public sealed partial class CollectionAssert { #region Singleton constructor @@ -29,1243 +29,6 @@ private CollectionAssert() #endregion - #region Membership - - /// - /// Tests whether the specified collection contains the specified element - /// and throws an exception if the element is not in the collection. - /// - /// - /// The collection in which to search for the element. - /// - /// - /// The element that is expected to be in the collection. - /// - /// - /// is null, or does not contain - /// element . - /// - public static void Contains([NotNull] ICollection? collection, object? element) - => Contains(collection, element, string.Empty); - - /// - /// Tests whether the specified collection contains the specified element - /// and throws an exception if the element is not in the collection. - /// - /// - /// The collection in which to search for the element. - /// - /// - /// The element that is expected to be in the collection. - /// - /// - /// The message to include in the exception when - /// is not in . The message is shown in - /// test results. - /// - /// - /// is null, or does not contain - /// element . - /// - public static void Contains([NotNull] ICollection? collection, object? element, string? message) - { - Assert.CheckParameterNotNull(collection, "CollectionAssert.Contains", "collection"); - - foreach (object? current in collection) - { - if (object.Equals(current, element)) - { - return; - } - } - - Assert.ReportAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); - } - - /// - /// Tests whether the specified collection does not contain the specified - /// element and throws an exception if the element is in the collection. - /// - /// - /// The collection in which to search for the element. - /// - /// - /// The element that is expected not to be in the collection. - /// - /// - /// is null, or contains - /// element . - /// - public static void DoesNotContain([NotNull] ICollection? collection, object? element) - => DoesNotContain(collection, element, string.Empty); - - /// - /// Tests whether the specified collection does not contain the specified - /// element and throws an exception if the element is in the collection. - /// - /// - /// The collection in which to search for the element. - /// - /// - /// The element that is expected not to be in the collection. - /// - /// - /// The message to include in the exception when - /// is in . The message is shown in test - /// results. - /// - /// - /// is null, or contains - /// element . - /// - public static void DoesNotContain([NotNull] ICollection? collection, object? element, string? message) - { - Assert.CheckParameterNotNull(collection, "CollectionAssert.DoesNotContain", "collection"); - - foreach (object? current in collection) - { - if (object.Equals(current, element)) - { - Assert.ReportAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); - } - } - } - - /// - /// Tests whether all items in the specified collection are non-null and throws - /// an exception if any element is null. - /// - /// - /// The collection in which to search for null elements. - /// - /// - /// is null, or contains a null element. - /// - public static void AllItemsAreNotNull([NotNull] ICollection? collection) - => AllItemsAreNotNull(collection, string.Empty); - - /// - /// Tests whether all items in the specified collection are non-null and throws - /// an exception if any element is null. - /// - /// - /// The collection in which to search for null elements. - /// - /// - /// The message to include in the exception when - /// contains a null element. The message is shown in test results. - /// - /// - /// is null, or contains a null element. - /// - public static void AllItemsAreNotNull([NotNull] ICollection? collection, string? message) - { - Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreNotNull", "collection"); - foreach (object? current in collection) - { - if (current == null) - { - Assert.ReportAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); - } - } - } - - /// - /// Tests whether all items in the specified collection are unique or not and - /// throws if any two elements in the collection are equal. - /// - /// - /// The collection in which to search for duplicate elements. - /// - /// - /// is null, or contains at least one duplicate - /// element. - /// - public static void AllItemsAreUnique([NotNull] ICollection? collection) - => AllItemsAreUnique(collection, string.Empty); - - /// - /// Tests whether all items in the specified collection are unique or not and - /// throws if any two elements in the collection are equal. - /// - /// - /// The collection in which to search for duplicate elements. - /// - /// - /// The message to include in the exception when - /// contains at least one duplicate element. The message is shown in - /// test results. - /// - /// - /// is null, or contains at least one duplicate - /// element. - /// - public static void AllItemsAreUnique([NotNull] ICollection? collection, string? message) - { - Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreUnique", "collection"); - - message = Assert.ReplaceNulls(message); - - bool foundNull = false; - HashSet table = []; - foreach (object? current in collection) - { - if (current == null) - { - if (!foundNull) - { - foundNull = true; - } - else - { - // Found a second occurrence of null. - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AllItemsAreUniqueFailMsg, - userMessage, - FrameworkMessages.Common_NullInMessages); - - Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); - } - } - else - { - if (!table.Add(current)) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AllItemsAreUniqueFailMsg, - userMessage, - Assert.ReplaceNulls(current)); - - Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); - } - } - } - } - - #endregion - - #region Subset - - /// - /// Tests whether one collection is a subset of another collection and - /// throws an exception if any element in the subset is not also in the - /// superset. - /// - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// is null, or is null, - /// or contains at least one element not contained in - /// . - /// - public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset) - => IsSubsetOf(subset, superset, string.Empty); - - /// - /// Tests whether one collection is a subset of another collection and - /// throws an exception if any element in the subset is not also in the - /// superset. - /// - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// The message to include in the exception when an element in - /// is not found in . - /// The message is shown in test results. - /// - /// - /// is null, or is null, - /// or contains at least one element not contained in - /// . - /// - public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset, string? message) - { - Assert.CheckParameterNotNull(subset, "CollectionAssert.IsSubsetOf", "subset"); - Assert.CheckParameterNotNull(superset, "CollectionAssert.IsSubsetOf", "superset"); - Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); - if (!isSubsetValue.Item1) - { - string returnedSubsetValueMessage = string.Join(", ", isSubsetValue.Item2.Select(item => Convert.ToString(item, CultureInfo.InvariantCulture))); - - returnedSubsetValueMessage = string.Format(CultureInfo.InvariantCulture, FrameworkMessages.ReturnedSubsetValueMessage, returnedSubsetValueMessage); - string userMessage = Assert.BuildUserMessage(message); - if (string.IsNullOrEmpty(userMessage)) - { - Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); - } - else - { - Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); - } - } - } - - /// - /// Tests whether one collection is not a subset of another collection and - /// throws an exception if all elements in the subset are also in the - /// superset. - /// - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// is null, or is null, - /// or all elements of are contained in . - /// - public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset) - => IsNotSubsetOf(subset, superset, string.Empty); - - /// - /// Tests whether one collection is not a subset of another collection and - /// throws an exception if all elements in the subset are also in the - /// superset. - /// - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// The message to include in the exception when every element in - /// is also found in . - /// The message is shown in test results. - /// - /// - /// is null, or is null, - /// or all elements of are contained in . - /// - public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] ICollection? superset, string? message) - { - Assert.CheckParameterNotNull(subset, "CollectionAssert.IsNotSubsetOf", "subset"); - Assert.CheckParameterNotNull(superset, "CollectionAssert.IsNotSubsetOf", "superset"); - Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); - if (isSubsetValue.Item1) - { - Assert.ReportAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); - } - } - - #endregion - - #region Equivalence - - /// - /// Tests whether two collections contain the same elements and throws an - /// exception if either collection contains an element not in the other - /// collection. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// and nullabilities don't match, - /// or if any element was found in one of the collections but not the other. - /// - public static void AreEquivalent( - [NotNullIfNotNull(nameof(actual))] ICollection? expected, [NotNullIfNotNull(nameof(expected))] ICollection? actual) - => AreEquivalent(expected?.Cast(), actual?.Cast(), EqualityComparer.Default, string.Empty); - - /// - /// Tests whether two collections contain the same elements and throws an - /// exception if either collection contains an element not in the other - /// collection. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The message to include in the exception when an element was found - /// in one of the collections but not the other. The message is shown - /// in test results. - /// - /// - /// and nullabilities don't match, - /// or if any element was found in one of the collections but not the other. - /// - public static void AreEquivalent( - [NotNullIfNotNull(nameof(actual))] ICollection? expected, [NotNullIfNotNull(nameof(expected))] ICollection? actual, string? message) - => AreEquivalent(expected?.Cast(), actual?.Cast(), EqualityComparer.Default, message); - - /// - /// Tests whether two collections contain the same elements and throws an - /// exception if either collection contains an element not in the other - /// collection. - /// - /// - /// The type of values to compare. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// and nullabilities don't match, - /// or if any element was found in one of the collections but not the other. - /// - public static void AreEquivalent( - [NotNullIfNotNull(nameof(actual))] IEnumerable? expected, [NotNullIfNotNull(nameof(expected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer) - => AreEquivalent(expected, actual, comparer, string.Empty); - - /// - /// Tests whether two collections contain the same elements and throws an - /// exception if either collection contains an element not in the other - /// collection. - /// - /// - /// The type of values to compare. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// The message to include in the exception when an element was found - /// in one of the collections but not the other. The message is shown - /// in test results. - /// - /// - /// and nullabilities don't match, - /// or if any element was found in one of the collections but not the other. - /// - public static void AreEquivalent( - [NotNullIfNotNull(nameof(actual))] IEnumerable? expected, [NotNullIfNotNull(nameof(expected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer, - string? message) - { - Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); - - // Check whether one is null while the other is not. - if (expected == null != (actual == null)) - { - Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); - } - - // If the references are the same or both collections are null, they are equivalent. - if (object.ReferenceEquals(expected, actual) || expected == null) - { - return; - } - - DebugEx.Assert(actual is not null, "actual is not null here"); - - int expectedCollectionCount = expected.Count(); - int actualCollectionCount = actual.Count(); - - // Check whether the element counts are different. - if (expectedCollectionCount != actualCollectionCount) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.ElementNumbersDontMatch, - userMessage, - expectedCollectionCount, - actualCollectionCount); - Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); - } - - // If both collections are empty, they are equivalent. - if (expectedCollectionCount == 0) - { - return; - } - - // Search for a mismatched element. - if (FindMismatchedElement(expected, actual, comparer, out int expectedCount, out int actualCount, out object? mismatchedElement)) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.ActualHasMismatchedElements, - userMessage, - expectedCount.ToString(CultureInfo.CurrentCulture.NumberFormat), - Assert.ReplaceNulls(mismatchedElement), - actualCount.ToString(CultureInfo.CurrentCulture.NumberFormat)); - Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); - } - - // All the elements and counts matched. - } - - /// - /// Tests whether two collections contain the different elements and throws an - /// exception if the two collections contain identical elements without regard - /// to order. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects to be different than the actual collection. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// and nullabilities don't match, - /// or if collections contain the same elements, including the same number of duplicate - /// occurrences of each element. - /// - public static void AreNotEquivalent( - [NotNullIfNotNull(nameof(actual))] ICollection? notExpected, [NotNullIfNotNull(nameof(notExpected))] ICollection? actual) - => AreNotEquivalent(notExpected?.Cast(), actual?.Cast(), EqualityComparer.Default, string.Empty); - - /// - /// Tests whether two collections contain the different elements and throws an - /// exception if the two collections contain identical elements without regard - /// to order. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects to be different than the actual collection. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The message to include in the exception when - /// contains the same elements as . The message - /// is shown in test results. - /// - /// - /// and nullabilities don't match, - /// or if collections contain the same elements, including the same number of duplicate - /// occurrences of each element. - /// - public static void AreNotEquivalent( - [NotNullIfNotNull(nameof(actual))] ICollection? notExpected, [NotNullIfNotNull(nameof(notExpected))] ICollection? actual, - string? message) - => AreNotEquivalent(notExpected?.Cast(), actual?.Cast(), comparer: EqualityComparer.Default, message); - - /// - /// Tests whether two collections contain the different elements and throws an - /// exception if the two collections contain identical elements without regard - /// to order. - /// - /// - /// The type of values to compare. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects to be different than the actual collection. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// and nullabilities don't match, - /// or if collections contain the same elements, including the same number of duplicate - /// occurrences of each element. - /// - public static void AreNotEquivalent( - [NotNullIfNotNull(nameof(actual))] IEnumerable? notExpected, [NotNullIfNotNull(nameof(notExpected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer) - => AreNotEquivalent(notExpected, actual, comparer, string.Empty); - - /// - /// Tests whether two collections contain the different elements and throws an - /// exception if the two collections contain identical elements without regard - /// to order. - /// - /// - /// The type of values to compare. - /// - /// - /// The first collection to compare. This contains the elements the test - /// expects to be different than the actual collection. - /// - /// - /// The second collection to compare. This is the collection produced by - /// the code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// The message to include in the exception when - /// contains the same elements as . The message - /// is shown in test results. - /// - /// - /// and nullabilities don't match, - /// or if collections contain the same elements, including the same number of duplicate - /// occurrences of each element. - /// - public static void AreNotEquivalent( - [NotNullIfNotNull(nameof(actual))] IEnumerable? notExpected, [NotNullIfNotNull(nameof(notExpected))] IEnumerable? actual, [NotNull] IEqualityComparer? comparer, - string? message) - { - Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); - - // Check whether one is null while the other is not. - if (notExpected == null != (actual == null)) - { - return; - } - - // If the references are the same or both collections are null, they - // are equivalent. object.ReferenceEquals will handle case where both are null. - if (object.ReferenceEquals(notExpected, actual)) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.BothCollectionsSameReference, - userMessage); - Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); - } - - DebugEx.Assert(actual is not null, "actual is not null here"); - DebugEx.Assert(notExpected is not null, "expected is not null here"); - - // Check whether the element counts are different. - int notExpectedCount = notExpected.Count(); - int actualCount = actual.Count(); - if (notExpectedCount != actualCount) - { - return; - } - - // If both collections are empty, they are equivalent. - if (notExpectedCount == 0) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.BothCollectionsEmpty, - userMessage); - Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); - } - - // Search for a mismatched element. - if (!FindMismatchedElement(notExpected, actual, comparer, out _, out _, out _)) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.BothSameElements, - userMessage); - Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); - } - } - - #endregion - - #region Type - - /// - /// Tests whether all elements in the specified collection are instances - /// of the expected type and throws an exception if the expected type is - /// not in the inheritance hierarchy of one or more of the elements. - /// - /// - /// The collection containing elements the test expects to be of the - /// specified type. - /// - /// - /// The expected type of each element of . - /// - /// - /// is null or, is null, - /// or some elements of do not inherit/implement - /// . - /// - public static void AllItemsAreInstancesOfType([NotNull] ICollection? collection, [NotNull] Type? expectedType) - => AllItemsAreInstancesOfType(collection, expectedType, string.Empty); - - /// - /// Tests whether all elements in the specified collection are instances - /// of the expected type and throws an exception if the expected type is - /// not in the inheritance hierarchy of one or more of the elements. - /// - /// - /// The collection containing elements the test expects to be of the - /// specified type. - /// - /// - /// The expected type of each element of . - /// - /// - /// The message to include in the exception when an element in - /// is not an instance of - /// . The message is shown in test results. - /// - /// - /// is null or, is null, - /// or some elements of do not inherit/implement - /// . - /// - public static void AllItemsAreInstancesOfType( - [NotNull] ICollection? collection, [NotNull] Type? expectedType, string? message) - { - Assert.CheckParameterNotNull(collection, "CollectionAssert.AllItemsAreInstancesOfType", "collection"); - Assert.CheckParameterNotNull(expectedType, "CollectionAssert.AllItemsAreInstancesOfType", "expectedType"); - int i = 0; - foreach (object? element in collection) - { - if (element?.GetType() is { } elementType - && !expectedType.IsAssignableFrom(elementType)) - { - string userMessage = Assert.BuildUserMessage(message); - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.ElementTypesAtIndexDontMatch, - userMessage, - i, - expectedType.ToString(), - element.GetType().ToString()); - Assert.ReportAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); - } - - i++; - } - } - - #endregion - - #region AreEqual - - /// - /// Tests whether the specified collections are equal and throws an exception - /// if the two collections are not equal. Equality is defined as having the same - /// elements in the same order and quantity. Whether two elements are the same - /// is checked using method. - /// Different references to the same value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects. - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// Thrown if is not equal to - /// . - /// - public static void AreEqual(ICollection? expected, ICollection? actual) - => AreEqual(expected, actual, string.Empty); - - /// - /// Tests whether the specified collections are equal and throws an exception - /// if the two collections are not equal. Equality is defined as having the same - /// elements in the same order and quantity. Whether two elements are the same - /// is checked using method. - /// Different references to the same value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects. - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The message to include in the exception when - /// is not equal to . The message is shown in - /// test results. - /// - /// - /// Thrown if is not equal to - /// . - /// - public static void AreEqual(ICollection? expected, ICollection? actual, string? message) - { - string reason = string.Empty; - if (!AreCollectionsEqual(expected, actual, new ObjectComparer(), ref reason)) - { - string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); - } - } - - /// - /// Tests whether the specified collections are unequal and throws an exception - /// if the two collections are equal. Equality is defined as having the same - /// elements in the same order and quantity. Whether two elements are the same - /// is checked using method. - /// Different references to the same value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects - /// not to match . - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// Thrown if is equal to . - /// - public static void AreNotEqual(ICollection? notExpected, ICollection? actual) - => AreNotEqual(notExpected, actual, string.Empty); - - /// - /// Tests whether the specified collections are unequal and throws an exception - /// if the two collections are equal. Equality is defined as having the same - /// elements in the same order and quantity. Whether two elements are the same - /// is checked using method. - /// Different references to the same value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects - /// not to match . - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The message to include in the exception when - /// is equal to . The message is shown in - /// test results. - /// - /// - /// Thrown if is equal to . - /// - public static void AreNotEqual(ICollection? notExpected, ICollection? actual, string? message) - { - string reason = string.Empty; - if (AreCollectionsEqual(notExpected, actual, new ObjectComparer(), ref reason)) - { - string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); - } - } - - /// - /// Tests whether the specified collections are equal and throws an exception - /// if the two collections are not equal. Equality is defined as having the same - /// elements in the same order and quantity. Different references to the same - /// value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects. - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// Thrown if is not equal to - /// . - /// - public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer) - => AreEqual(expected, actual, comparer, string.Empty); - - /// - /// Tests whether the specified collections are equal and throws an exception - /// if the two collections are not equal. Equality is defined as having the same - /// elements in the same order and quantity. Different references to the same - /// value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects. - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// The message to include in the exception when - /// is not equal to . The message is shown in - /// test results. - /// - /// - /// Thrown if is not equal to - /// . - /// - public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer, string? message) - { - string reason = string.Empty; - if (!AreCollectionsEqual(expected, actual, comparer, ref reason)) - { - string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); - } - } - - /// - /// Tests whether the specified collections are unequal and throws an exception - /// if the two collections are equal. Equality is defined as having the same - /// elements in the same order and quantity. Different references to the same - /// value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects - /// not to match . - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// Thrown if is equal to . - /// - public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [NotNull] IComparer? comparer) - => AreNotEqual(notExpected, actual, comparer, string.Empty); - - /// - /// Tests whether the specified collections are unequal and throws an exception - /// if the two collections are equal. Equality is defined as having the same - /// elements in the same order and quantity. Different references to the same - /// value are considered equal. - /// - /// - /// The first collection to compare. This is the collection the tests expects - /// not to match . - /// - /// - /// The second collection to compare. This is the collection produced by the - /// code under test. - /// - /// - /// The compare implementation to use when comparing elements of the collection. - /// - /// - /// The message to include in the exception when - /// is equal to . The message is shown in - /// test results. - /// - /// - /// Thrown if is equal to . - /// - public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [NotNull] IComparer? comparer, string? message) - { - string reason = string.Empty; - if (AreCollectionsEqual(notExpected, actual, comparer, ref reason)) - { - string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); - } - } - - #endregion - - #region Helpers - - /// - /// Determines whether the first collection is a subset of the second - /// collection. If either set contains duplicate elements, the number - /// of occurrences of the element in the subset must be less than or - /// equal to the number of occurrences in the superset. - /// - /// - /// The collection the test expects to be contained in . - /// - /// - /// The collection the test expects to contain . - /// - /// - /// True if is a subset of - /// , false otherwise. - /// - internal static Tuple> IsSubsetOfHelper(ICollection subset, ICollection superset) - { - // $ CONSIDER: The current algorithm counts the number of occurrences of each - // $ CONSIDER: element in each collection and then compares the count, resulting - // $ CONSIDER: in an algorithm of ~n*log(n) + m*log(m) + n*log(m). It should be - // $ CONSIDER: faster to sort both collections and do an element-by-element - // $ CONSIDER: comparison, which should result in ~n*log(n) + m*log(m) + n. - var nonSubsetValues = new List(); - - // Count the occurrences of each object in both collections. - Dictionary subsetElements = GetElementCounts(subset.Cast(), EqualityComparer.Default, out int subsetNulls); - Dictionary supersetElements = GetElementCounts(superset.Cast(), EqualityComparer.Default, out int supersetNulls); - - bool isSubset = true; - - // Check null counts first - if (subsetNulls > supersetNulls) - { - isSubset = false; - // Add the excess null values to non-subset collection - for (int i = 0; i < (subsetNulls - supersetNulls); i++) - { - nonSubsetValues.Add(null); - } - } - - // Compare the counts of each object in the subset to the count of that object - // in the superset. - foreach (object? element in subsetElements.Keys) - { - subsetElements.TryGetValue(element, out int subsetCount); - supersetElements.TryGetValue(element, out int supersetCount); - - if (subsetCount > supersetCount) - { - isSubset = false; - // Add the excess occurrences to non-subset collection - int excessCount = subsetCount - supersetCount; - for (int i = 0; i < excessCount; i++) - { - nonSubsetValues.Add(element); - } - } - } - - return new Tuple>(isSubset, nonSubsetValues); - } - -#pragma warning disable CS8714 - /// - /// Constructs a dictionary containing the number of occurrences of each - /// element in the specified collection. - /// - /// - /// The collection to process. - /// - /// The equality comparer to use when comparing items. - /// - /// The number of null elements in the collection. - /// - /// - /// A dictionary containing the number of occurrences of each element - /// in the specified collection. - /// - private static Dictionary GetElementCounts(IEnumerable collection, IEqualityComparer comparer, out int nullCount) - { - DebugEx.Assert(collection != null, "Collection is Null."); - - var elementCounts = new Dictionary(comparer); - nullCount = 0; - - foreach (T? element in collection) - { - if (element == null) - { - nullCount++; - continue; - } - - elementCounts.TryGetValue(element, out int value); - value++; - elementCounts[element] = value; - } - - return elementCounts; - } - - /// - /// Finds a mismatched element between the two collections. A mismatched - /// element is one that appears a different number of times in the - /// expected collection than it does in the actual collection. The - /// collections are assumed to be different non-null references with the - /// same number of elements. The caller is responsible for this level of - /// verification. If there is no mismatched element, the function returns - /// false and the out parameters should not be used. - /// - /// - /// The first collection to compare. - /// - /// - /// The second collection to compare. - /// - /// The equality comparer to use when comparing items. - /// - /// The expected number of occurrences of - /// or 0 if there is no mismatched - /// element. - /// - /// - /// The actual number of occurrences of - /// or 0 if there is no mismatched - /// element. - /// - /// - /// The mismatched element (may be null) or null if there is no - /// mismatched element. - /// - /// - /// true if a mismatched element was found; false otherwise. - /// - private static bool FindMismatchedElement(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer, out int expectedCount, - out int actualCount, out object? mismatchedElement) - { - // $ CONSIDER: The current algorithm counts the number of occurrences of each - // $ CONSIDER: element in each collection and then compares the count, resulting - // $ CONSIDER: in an algorithm of ~n*log(n) + m*log(m) + n*log(m). It should be - // $ CONSIDER: faster to sort both collections and do an element-by-element - // $ CONSIDER: comparison, which should result in ~n*log(n) + m*log(m) + n. - - // Count the occurrences of each object in the both collections - Dictionary expectedElements = GetElementCounts(expected, comparer, out int expectedNulls); - Dictionary actualElements = GetElementCounts(actual, comparer, out int actualNulls); - - if (actualNulls != expectedNulls) - { - expectedCount = expectedNulls; - actualCount = actualNulls; - mismatchedElement = null; - return true; - } - - // Compare the counts of each object. Note that this comparison only needs - // to be done one way since comparing the total count is a prerequisite to - // calling this function. - foreach (T current in expectedElements.Keys) - { - expectedElements.TryGetValue(current, out expectedCount); - actualElements.TryGetValue(current, out actualCount); - - if (expectedCount != actualCount) - { - mismatchedElement = current; - return true; - } - } - - // All the elements and counts matched. - expectedCount = 0; - actualCount = 0; - mismatchedElement = null; - return false; - } -#pragma warning restore CS8714 - - private static bool AreCollectionsEqual(ICollection? expected, ICollection? actual, [NotNull] IComparer? comparer, - ref string reason) - { - Assert.CheckParameterNotNull(comparer, "Assert.AreCollectionsEqual", "comparer"); - if (object.ReferenceEquals(expected, actual)) - { - reason = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsSameReference, string.Empty); - return true; - } - - return CompareIEnumerable(expected, actual, comparer, ref reason); - } - - private static bool CompareIEnumerable(IEnumerable? expected, IEnumerable? actual, IComparer comparer, ref string reason) - { - if ((expected == null) || (actual == null)) - { - return false; - } - - var stack = new Stack>(); - stack.Push(new(expected.GetEnumerator(), actual.GetEnumerator(), 0)); - - while (stack.Count > 0) - { - Tuple cur = stack.Pop(); - IEnumerator expectedEnum = cur.Item1; - IEnumerator actualEnum = cur.Item2; - int position = cur.Item3; - - while (expectedEnum.MoveNext()) - { - if (!actualEnum.MoveNext()) - { - reason = FrameworkMessages.NumberOfElementsDiff; - return false; - } - - object? curExpected = expectedEnum.Current; - object? curActual = actualEnum.Current; - if (comparer.Compare(curExpected, curActual) == 0) - { - position++; - } - else if (curExpected is IEnumerable curExpectedEnum && curActual is IEnumerable curActualEnum) - { - stack.Push(new(expectedEnum, actualEnum, position + 1)); - stack.Push(new(curExpectedEnum.GetEnumerator(), curActualEnum.GetEnumerator(), 0)); - } - else - { - reason = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.ElementsAtIndexDontMatch, - position, - Assert.ReplaceNulls(curExpected), - Assert.ReplaceNulls(curActual)); - return false; - } - } - - if (actualEnum.MoveNext() && !expectedEnum.MoveNext()) - { - reason = FrameworkMessages.NumberOfElementsDiff; - return false; - } - } - - reason = FrameworkMessages.BothCollectionsSameElements; - return true; - } - - private static string ConstructFinalMessage( - string reason, - string? message) - { - string userMessage = Assert.BuildUserMessage(message); - return userMessage.Length == 0 - ? reason - : string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CollectionEqualReason, userMessage, reason); - } - - /// - /// compares the objects using object.Equals. - /// - private sealed class ObjectComparer : IComparer - { - int IComparer.Compare(object? x, object? y) => Equals(x, y) ? 0 : -1; - } - #endregion - #region DoNotUse /// diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs index 0b8aafbaaf..41ee5759ae 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs @@ -31,7 +31,7 @@ public async Task AbortWithCTRLPlusC_CancellingTests(string tfm) }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestSessionAborted); + testHostResult.AssertExitCodeIs(ExitCode.TestSessionAborted); testHostResult.AssertOutputMatchesRegex("Canceling the test session.*"); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyCleanupTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyCleanupTests.cs index 3de2a389e6..bf248ba0ed 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyCleanupTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyCleanupTests.cs @@ -16,7 +16,7 @@ public async Task AssemblyCleanupShouldRunAfterAllClassCleanupsHaveCompleted() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0); testHostResult.AssertOutputContains(""" TestClass1.Test1. diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyResolverTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyResolverTests.cs index 52d1973dcd..5c3ee8a84d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyResolverTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyResolverTests.cs @@ -21,7 +21,7 @@ public async Task RunningTests_DoesNotHitResourceRecursionIssueAndDoesNotCrashTh TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } public sealed class TestAssetFixture() : TestAssetFixtureBase() diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestSettingsTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestSettingsTests.cs index 093b0c95bd..a9fbdc7fb1 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestSettingsTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestSettingsTests.cs @@ -20,7 +20,7 @@ public async Task TestConfigJson_AndRunSettingsHasMstest_Throws(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertStandardErrorContains("Both '.runsettings' and '.testconfig.json' files have been detected. Please select only one of these test configuration files."); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestV2SettingsTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestV2SettingsTests.cs index 818b569849..dc745a8bc3 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestV2SettingsTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationMSTestV2SettingsTests.cs @@ -20,7 +20,7 @@ public async Task TestConfigJson_AndRunSettingsHasMstestv2_Throws(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertStandardErrorContains("Both '.runsettings' and '.testconfig.json' files have been detected. Please select only one of these test configuration files."); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationSettingsTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationSettingsTests.cs index 590fc9bc3a..7cc0b550c4 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationSettingsTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ConfigurationSettingsTests.cs @@ -18,7 +18,7 @@ public async Task TestConfigJson_AndRunSettingsWithoutMstest_OverrideRunConfigra TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } [TestMethod] @@ -29,7 +29,7 @@ public async Task TestConfigJson_WithoutRunSettings_BuildSuccess(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } [TestMethod] @@ -44,7 +44,7 @@ public async Task TestWithConfigFromCommandLineWithMapInconclusiveToFailedIsTrue }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContainsSummary(failed: 1, passed: 1, skipped: 0); } @@ -60,7 +60,7 @@ public async Task TestWithConfigFromCommandLineWithMapInconclusiveToFailedIsFals }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 1); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CustomAttributesTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CustomAttributesTests.cs index 043172a089..c6e509236f 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CustomAttributesTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CustomAttributesTests.cs @@ -19,7 +19,7 @@ public async Task DuplicateTestMethodAttribute_ShouldFail(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.DuplicateTestMethodProjectPath, TestAssetFixture.DuplicateTestMethodProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContainsSummary(failed: 1, passed: 1, skipped: 0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs index 3fab228226..08a11f4ba6 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DataSourceTests.cs @@ -101,7 +101,7 @@ await DotnetCli.RunAsync( var testHost = TestHost.LocateFrom(generator.TargetAssetPath, "DataSourceTests", "net472"); TestHostResult result = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - result.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + result.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); result.AssertOutputContainsSummary(failed: 1, passed: 4, skipped: 0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DeploymentItemTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DeploymentItemTests.cs index 811036e96e..2447986f4f 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DeploymentItemTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DeploymentItemTests.cs @@ -21,7 +21,7 @@ public async Task AssemblyIsLoadedOnceFromDeploymentDirectory(string runsettings var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetFramework[0]); TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runsettings}", cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } public sealed class TestAssetFixture() : TestAssetFixtureBase() diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DuplicateTestClassAttributeTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DuplicateTestClassAttributeTests.cs index 61fe02fc7d..6540950f36 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DuplicateTestClassAttributeTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DuplicateTestClassAttributeTests.cs @@ -19,7 +19,7 @@ public async Task DuplicateTestClassAttribute_ShouldFail(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.DuplicateTestClassProjectPath, TestAssetFixture.DuplicateTestClassProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertStandardErrorContains("Only one attribute of type 'Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute' is allowed, but multiple were found."); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DynamicDataMethodTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DynamicDataMethodTests.cs index 988ab77806..515136960d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DynamicDataMethodTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DynamicDataMethodTests.cs @@ -17,7 +17,7 @@ public async Task DynamicDataTestWithParameterizedDataProviderMethod(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContainsSummary(failed: 3, passed: 9, skipped: 0); // failed TestMethodSingleParameterIntCountMismatchSmaller (0ms) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/FrameworkOnlyTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/FrameworkOnlyTests.cs index efbfe3a196..d0ae8a5632 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/FrameworkOnlyTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/FrameworkOnlyTests.cs @@ -25,7 +25,7 @@ public async Task DynamicDataAttributeGetDataShouldWorkWithoutAdapter() 3,4 """); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } public sealed class TestAssetFixture() : TestAssetFixtureBase() diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/GenericTestMethodTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/GenericTestMethodTests.cs index b51b183db3..6581de34dc 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/GenericTestMethodTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/GenericTestMethodTests.cs @@ -17,7 +17,7 @@ public async Task TestDifferentGenericMethodTestCases() TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputMatchesRegex( """ failed AMethodWithBadConstraints \(0\) \((\d+s )?\d+ms\) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/HelpInfoTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/HelpInfoTests.cs index 30588c061c..58c3871342 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/HelpInfoTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/HelpInfoTests.cs @@ -19,7 +19,7 @@ public async Task Help_WhenMSTestExtensionRegistered_OutputHelpContentOfRegister var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string wildcardMatchPattern = $""" MSTest v{MSTestVersion} (UTC *) [* - *] @@ -101,7 +101,7 @@ public async Task Info_WhenMSTestExtensionRegistered_OutputInfoContentOfRegister var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--info", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string output = $""" MSTestExtension diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/IgnoreTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/IgnoreTests.cs index 6c75ea6d43..0bb2606c8b 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/IgnoreTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/IgnoreTests.cs @@ -17,7 +17,7 @@ public async Task ClassCleanup_Inheritance_WhenClassIsSkipped() TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings --filter ClassName!~TestClassWithAssemblyInitialize", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 11, skipped: 8); testHostResult.AssertOutputContains("SubClass.Method"); @@ -32,7 +32,7 @@ public async Task WhenAllTestsAreIgnored_AssemblyInitializeAndCleanupAreSkipped( TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings --filter TestClassWithAssemblyInitialize", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 0, skipped: 1); testHostResult.AssertOutputDoesNotContain("AssemblyInitialize"); testHostResult.AssertOutputDoesNotContain("AssemblyCleanup"); @@ -45,7 +45,7 @@ public async Task WhenSpecificDataSourceIsIgnoredViaIgnoreMessageProperty() TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings --filter TestClassWithDataSourcesUsingIgnoreMessage", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("TestInitialize: TestMethod1 (0)"); testHostResult.AssertOutputContains("TestCleanup: TestMethod1 (0)"); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/InconclusiveTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/InconclusiveTests.cs index d1230ef4d7..9f5a931991 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/InconclusiveTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/InconclusiveTests.cs @@ -44,12 +44,12 @@ public async Task TestOutcomeShouldBeRespectedCorrectly(Lifecycle inconclusiveSt if (inconclusiveStep >= Lifecycle.ClassCleanup) { - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContainsSummary(failed: 1, passed: 1, skipped: 0); } else { - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 0, skipped: 1); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/LifecycleTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/LifecycleTests.cs index fbfe7d1b92..df19e07c16 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/LifecycleTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/LifecycleTests.cs @@ -17,7 +17,7 @@ public async Task LifecycleTest(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 4, skipped: 0); // Order is: // - Assembly initialize diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs index aa0cef38b4..bb5dc9e8c0 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs @@ -19,7 +19,7 @@ public async Task SimpleMaxFailedTestsScenario(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--maximum-failed-tests 3", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestExecutionStoppedForMaxFailedTests); + testHostResult.AssertExitCodeIs(ExitCode.TestExecutionStoppedForMaxFailedTests); int total = int.Parse(Regex.Match(testHostResult.StandardOutput, @"total: (\d+)").Groups[1].Value, CultureInfo.InvariantCulture); @@ -30,7 +30,7 @@ public async Task SimpleMaxFailedTestsScenario(string tfm) Assert.IsGreaterThanOrEqualTo(5, total); testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); total = int.Parse(Regex.Match(testHostResult.StandardOutput, @"total: (\d+)").Groups[1].Value, CultureInfo.InvariantCulture); Assert.AreEqual(12, total); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataRowTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataRowTests.cs index aec64687dd..d84654b464 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataRowTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataRowTests.cs @@ -28,7 +28,7 @@ private static async Task UsingDataRowThatDoesNotRoundTripUsingDataContractJsonS TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettings}.runsettings --filter ClassName=ParameterizedTestSerializationIssue2390"); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 3, skipped: 0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataSourceTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataSourceTests.cs index f5a552beb9..fface70796 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataSourceTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedDataSourceTests.cs @@ -35,7 +35,7 @@ private static async Task RunTestsAsync(string currentTfm, string assetName, boo bool isSuccess = isEmptyDataInconclusive.HasValue && isEmptyDataInconclusive.Value; - testHostResult.AssertExitCodeIs(isSuccess ? ExitCodes.Success : ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(isSuccess ? ExitCode.Success : ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains(isSuccess ? "skipped Test" : "failed Test"); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedTestTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedTestTests.cs index e8de47e559..b7943d0f7c 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedTestTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ParameterizedTestTests.cs @@ -58,7 +58,7 @@ public async Task UsingTestDataRowVariousCases(string currentTfm) // progress causes flakiness. See https://github.com/microsoft/testfx/pull/4930#issuecomment-2648506466 testHostResult = await testHost.ExecuteAsync("--filter ClassName=TestDataRowTests --no-progress", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 9, skipped: 15); // If this assert fails with difference showing only missing double quotes, then we are using the wrong value // of DynamicDataAttribute.TestIdGenerationStrategy. @@ -107,7 +107,7 @@ private static async Task RunTestsAsync(string currentTfm, string assetName, boo bool isSuccess = isEmptyDataInconclusive.HasValue && isEmptyDataInconclusive.Value; - testHostResult.AssertExitCodeIs(isSuccess ? ExitCodes.Success : ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(isSuccess ? ExitCode.Success : ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains(isSuccess ? "skipped Test" : "failed Test"); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RetryTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RetryTests.cs index 4b34bb1b53..4d8ff10a08 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RetryTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RetryTests.cs @@ -16,7 +16,7 @@ public async Task BasicRetryScenarioTest() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains(""" TestMethod1 executed 1 time. TestMethod2 executed 2 times. diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index ef56b1ce7f..934d962192 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -226,7 +226,7 @@ public async Task RunTests_With_MSTestRunner_Standalone_Selectively_Enabled_Exte testHostResult.AssertOutputContainsSummary(0, 1, 0); testHostResult = await testHost.ExecuteAsync(command: invalidCommandLineArg, cancellationToken: TestContext.CancellationToken); - Assert.AreEqual(ExitCodes.InvalidCommandLine, testHostResult.ExitCode); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); } } @@ -283,7 +283,7 @@ public async Task RunTests_With_MSTestRunner_Standalone_Enable_Default_Extension } else { - Assert.AreEqual(ExitCodes.InvalidCommandLine, testHostResult.ExitCode); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); } } } @@ -329,7 +329,7 @@ public async Task NativeAot_Smoke_Test() var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, verb: Verb.publish); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(0, 1, 0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ShowOutputOptionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ShowOutputOptionTests.cs index 4756c09108..69ec1667da 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ShowOutputOptionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ShowOutputOptionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Platform.Acceptance.IntegrationTests; @@ -72,7 +72,7 @@ public async Task ShowStdout_InvalidArgument_ReturnsError(string tfm) "--show-stdout invalid", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("--show-stdout and --show-stderr expect a single parameter with value 'All', 'Failed', or 'None'."); } @@ -138,7 +138,7 @@ public async Task ShowStderr_InvalidArgument_ReturnsError(string tfm) "--show-stderr invalid", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("--show-stdout and --show-stderr expect a single parameter with value 'All', 'Failed', or 'None'."); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs index b0e86a1dbe..ebef8904a1 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -16,7 +16,7 @@ public async Task ScopeWithNoFailures_TestPasses() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithNoFailures", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } @@ -26,7 +26,7 @@ public async Task ScopeWithSingleFailure_TestFails() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithSingleFailure", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputMatchesRegex( """failed ScopeWithSingleFailure \(\d+ms\)[\s\S]+Assert\.AreEqual failed\. Expected:<1>\. Actual:<2>\.[\s\S]+at UnitTest1\.ScopeWithSingleFailure\(\)"""); } @@ -37,7 +37,7 @@ public async Task ScopeWithMultipleFailures_TestFailsWithAggregatedMessage() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithMultipleFailures", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); // Validate the output includes the aggregate message and that inner exception stack traces // point to the test method (assertion call site). testHostResult.AssertOutputMatchesRegex( @@ -50,7 +50,7 @@ public async Task AssertFailIsHardFailure_ThrowsImmediately() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter AssertFailIsHardFailure", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); // Assert.Fail is a hard assertion — it throws immediately, even within a scope. // The second Assert.Fail should not be reached. testHostResult.AssertOutputMatchesRegex( @@ -64,7 +64,7 @@ public async Task ScopeWithSoftFailureFollowedByException_CollectsBoth() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter SoftFailureFollowedByException", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputMatchesRegex( """failed SoftFailureFollowedByException \(\d+ms\)[\s\S]+at UnitTest1\.SoftFailureFollowedByException\(\)"""); } @@ -75,7 +75,7 @@ public async Task ScopeWithIsNotNullSoftFailure_CollectsFailure() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithIsNotNullSoftFailure", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputMatchesRegex( """failed ScopeWithIsNotNullSoftFailure \(\d+ms\)[\s\S]+Assert\.IsNotNull failed\.[\s\S]+at UnitTest1\.ScopeWithIsNotNullSoftFailure\(\)"""); } @@ -86,7 +86,7 @@ public async Task ScopeAssertionsAreIndependentBetweenTests_SecondTestPasses() var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter IndependentTest", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryTests.cs index 11ac63ef82..8a75963330 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryTests.cs @@ -20,7 +20,7 @@ public async Task DiscoverTests_FindsAllTests(string currentTfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Test1"); testHostResult.AssertOutputContains("Test2"); testHostResult.AssertOutputContains("Display name: 1, one"); @@ -35,7 +35,7 @@ public async Task DiscoverTests_WithFilter_FindsOnlyFilteredOnes(string currentT TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests --filter Name=Test1", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Test1"); testHostResult.AssertOutputDoesNotContain("Test2"); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryWarningsTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryWarningsTests.cs index b970ea22a4..13e7724149 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryWarningsTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestDiscoveryWarningsTests.cs @@ -27,7 +27,7 @@ public async Task DiscoverTests_ShowsWarningsForTestsThatFailedToDiscover(string // We check for appdomain directly in the test, so if tests fail we did not run in appdomain. TestHostResult testHostSuccessResult = await testHost.ExecuteAsync("--settings AppDomainEnabled.runsettings", cancellationToken: TestContext.CancellationToken); - testHostSuccessResult.AssertExitCodeIs(ExitCodes.Success); + testHostSuccessResult.AssertExitCodeIs(ExitCode.Success); } // Delete the TestDiscoveryWarningsBaseClass.dll from the test bin folder on purpose, to break discovering @@ -36,7 +36,7 @@ public async Task DiscoverTests_ShowsWarningsForTestsThatFailedToDiscover(string TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); if (isNetFx) { testHostResult.AssertStandardErrorContains("Could not load file or assembly 'TestDiscoveryWarningsBaseClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified."); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs index 7eccd640c5..bcf7f69047 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs @@ -20,7 +20,7 @@ public async Task RunWithFilter_UsingTestProperty_FilteredTests(string currentTf TestHostResult testHostResult = await testHost.ExecuteAsync("--filter tree=one", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } @@ -32,7 +32,7 @@ public async Task DiscoverTestsWithFilter_UsingTestProperty_FilteredTests(string TestHostResult testHostResult = await testHost.ExecuteAsync("--filter tree=one --list-tests", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputMatchesRegex(""" Test2 Test discovery summary: found 1 test\(s\)\ - .*\.(dll|exe) \(net.+\|.+\) @@ -62,25 +62,25 @@ public async Task RunWithFilterFromRunsettings(string currentTfm) testHostResult.AssertOutputContains("Running test: CategoryAOnly"); testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); testHostResult.AssertOutputContains("Running test: CategoryAAndB"); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult = await testHost.ExecuteAsync("--settings NoFilter.runsettings", cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContains("Running test: CategoryAOnly"); testHostResult.AssertOutputContains("Running test: CategoryBOnly"); testHostResult.AssertOutputContains("Running test: CategoryAAndB"); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult = await testHost.ExecuteAsync("--settings CategoryA.runsettings --filter TestCategory~CategoryA", cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContains("Running test: CategoryAOnly"); testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); testHostResult.AssertOutputContains("Running test: CategoryAAndB"); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult = await testHost.ExecuteAsync("--settings CategoryA.runsettings --filter TestCategory~CategoryB", cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputDoesNotContain("Running test: CategoryAOnly"); testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); testHostResult.AssertOutputContains("Running test: CategoryAAndB"); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } public sealed class TestAssetFixture() : TestAssetFixtureBase() diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutCooperativeTestMethodTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutCooperativeTestMethodTests.cs index 437d80b1aa..f48d11235c 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutCooperativeTestMethodTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutCooperativeTestMethodTests.cs @@ -22,7 +22,7 @@ public async Task CooperativeTimeout_WhenMethodTimeoutAndWaitInCtor_TestGetsCanc }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } @@ -38,7 +38,7 @@ public async Task CooperativeTimeout_WhenMethodTimeoutAndWaitInTestInit_TestGets }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test initialize method 'TimeoutTest.UnitTest1.TestInit' timed out after 1000ms"); } @@ -54,7 +54,7 @@ public async Task CooperativeTimeout_WhenMethodTimeoutAndWaitInTestCleanup_TestG }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test cleanup method 'TimeoutTest.UnitTest1.TestCleanup' timed out after 1000ms"); } @@ -70,7 +70,7 @@ public async Task CooperativeTimeout_WhenMethodTimeoutAndWaitInTestMethod_TestGe }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTestMethodTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTestMethodTests.cs index ea164974ad..78c59dbe1c 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTestMethodTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTestMethodTests.cs @@ -22,7 +22,7 @@ public async Task Timeout_WhenMethodTimeoutAndWaitInCtor_TestGetsCanceled(string }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } @@ -38,7 +38,7 @@ public async Task Timeout_WhenMethodTimeoutAndWaitInTestInit_TestGetsCanceled(st }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } @@ -54,7 +54,7 @@ public async Task Timeout_WhenMethodTimeoutAndWaitInTestCleanup_TestGetsCanceled }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } @@ -70,7 +70,7 @@ public async Task Timeout_WhenMethodTimeoutAndWaitInTestMethod_TestGetsCanceled( }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Test 'TestMethod' timed out after 1000ms"); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTests.cs index 1e7dbe6ae5..5f59c7e7db 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TimeoutTests.cs @@ -17,7 +17,7 @@ public async Task TimeoutWithInvalidArg_WithoutLetterSuffix_OutputInvalidMessage var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -28,7 +28,7 @@ public async Task TimeoutWithInvalidArg_WithInvalidLetterSuffix_OutputInvalidMes var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5y", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -39,7 +39,7 @@ public async Task TimeoutWithInvalidArg_WithInvalidFormat_OutputInvalidMessage(s var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5h6m", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -50,7 +50,7 @@ public async Task Timeout_WhenTimeoutValueSmallerThanTestDuration_OutputContains var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 1s", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("Canceling the test session"); } @@ -61,7 +61,7 @@ public async Task Timeout_WhenTimeoutValueGreaterThanTestDuration_OutputDoesNotC var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 30s", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputDoesNotContain("Canceling the test session"); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrxReportTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrxReportTests.cs index 5453af5894..dfcce5307f 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrxReportTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrxReportTests.cs @@ -18,7 +18,7 @@ public async Task TrxReport_WhenTestFails_ContainsExceptionInfoInOutput(string t var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {fileName}.trx", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); string trxFile = Directory.GetFiles(testHost.DirectoryName, $"{fileName}.trx", SearchOption.AllDirectories).Single(); string trxContent = File.ReadAllText(trxFile); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TupleDynamicDataTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TupleDynamicDataTests.cs index 6beef180cf..d44487732d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TupleDynamicDataTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TupleDynamicDataTests.cs @@ -18,7 +18,7 @@ public async Task CanUseLongTuplesAndValueTuplesForAllFrameworks(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ClassName=CanUseLongTuplesAndValueTuplesForAllFrameworks --settings my.runsettings", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains(""" 1, 2, 3, 4, 5, 6, 7, 8 9, 10, 11, 12, 13, 14, 15, 16 @@ -44,7 +44,7 @@ public async Task TupleSupportDoesNotBreakObjectArraySupport(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ClassName=TupleSupportDoesNotBreakObjectArraySupport --settings my.runsettings", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains(""" Length: 1 (Hello, World) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/WinUITests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/WinUITests.cs index e2d4531f84..463b27d9df 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/WinUITests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/WinUITests.cs @@ -20,7 +20,7 @@ public async Task SimpleWinUITestCase() TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/AbortionTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/AbortionTests.cs index 1d64bedf6c..9895a3bf64 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/AbortionTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/AbortionTests.cs @@ -19,7 +19,7 @@ public async Task AbortWithCTRLPlusC_TestHost_Succeeded(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestSessionAborted); + testHostResult.AssertExitCodeIs(ExitCode.TestSessionAborted); // We don't assert "Canceling the test session" message. // Cancellation could happen very first that we didn't have the opportunity to write this message. diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ConsoleTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ConsoleTests.cs index 377578b012..67556103b1 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ConsoleTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ConsoleTests.cs @@ -49,7 +49,7 @@ public async Task ProgressAndControllerOutputAreNotFullySynchronizedAcrossProces $"\"{testHost.FullName}\" --ignore-exit-code 8", cancellationToken: TestContext.CancellationToken); - Assert.AreEqual(ExitCodes.Success, exitCode); + Assert.AreEqual((int)ExitCode.Success, exitCode); Assert.Contains("Slowest 10 tests", commandLine.StandardOutput); } @@ -68,7 +68,7 @@ private async Task ConsoleTestsCoreAsync(string tfm, string? environmentVariable } TestHostResult testHostResult = await testHost.ExecuteAsync("--ignore-exit-code 8", environmentVariables, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("ABCDEF123"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs index 77b69bc77a..cf7e844be3 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs @@ -13,7 +13,7 @@ public async Task CrashDump_DefaultSetting_CreateDump(string tfm) string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--crashdump --results-directory {resultDirectory}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string? dumpFile = Directory.GetFiles(resultDirectory, "CrashDump_*.dmp", SearchOption.AllDirectories).SingleOrDefault(); Assert.IsNotNull(dumpFile, $"Dump file not found '{tfm}'\n{testHostResult}'"); } @@ -24,7 +24,7 @@ public async Task CrashDump_CustomDumpName_CreateDump() string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync($"--crashdump --crashdump-filename customdumpname.dmp --results-directory {resultDirectory}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); Assert.ContainsSingle(Directory.GetFiles(resultDirectory, "customdumpname.dmp", SearchOption.AllDirectories), $"Dump file not found\n{testHostResult}"); } @@ -38,7 +38,7 @@ public async Task CrashDump_Formats_CreateDump(string format) string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync($"--crashdump --crashdump-type {format} --results-directory {resultDirectory}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string dumpFile = Assert.ContainsSingle(Directory.GetFiles(resultDirectory, "CrashDump_*.dmp", SearchOption.AllDirectories), $"Dump file not found '{format}'\n{testHostResult}"); File.Delete(dumpFile); @@ -50,7 +50,7 @@ public async Task CrashDump_InvalidFormat_ShouldFail() string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync($"--crashdump --crashdump-type invalid --results-directory {resultDirectory}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Option '--crashdump-type' has invalid arguments: 'invalid' is not a valid dump type. Valid options are 'Mini', 'Heap', 'Triage' and 'Full'"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashPlusHangDumpTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashPlusHangDumpTests.cs index 7ef7a62030..ac53ec2e5d 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashPlusHangDumpTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashPlusHangDumpTests.cs @@ -22,7 +22,7 @@ public async Task CrashPlusHangDump_InCaseOfCrash_CreateCrashDump() }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult.AssertOutputMatchesRegex(@"Test host process with PID \'.+\' crashed, a dump file was generated"); testHostResult.AssertOutputDoesNotContain(@"Hang dump timeout '00:00:08' expired"); @@ -46,7 +46,7 @@ public async Task CrashPlusHangDump_InCaseOfHang_CreateHangDump() }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult.AssertOutputDoesNotMatchRegex(@"Test host process with PID '.+' crashed, a dump file was generated"); testHostResult.AssertOutputContains(@"Hang dump timeout of '00:00:08' expired"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CustomBannerTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CustomBannerTests.cs index 5777814564..809ae43600 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CustomBannerTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CustomBannerTests.cs @@ -15,7 +15,7 @@ public async Task UsingNoBanner_TheBannerDoesNotAppear(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--no-banner", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain(TestAssetFixture.CustomBannerPrefix); } @@ -32,7 +32,7 @@ public async Task UsingNoBanner_InTheEnvironmentVars_TheBannerDoesNotAppear(stri }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain(TestAssetFixture.CustomBannerPrefix); } @@ -49,7 +49,7 @@ public async Task UsingDotnetNoLogo_InTheEnvironmentVars_TheBannerDoesNotAppear( }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain(TestAssetFixture.CustomBannerPrefix); } @@ -60,7 +60,7 @@ public async Task WithoutUsingNoBanner_TheBannerAppears(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputMatchesRegex($"{TestAssetFixture.CustomBannerPrefix} Platform info: Name: Microsoft.Testing.Platform, Version: .+?, Hash: .*?, Date: .+?"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DataConsumerThroughputTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DataConsumerThroughputTests.cs index a0d318e1b3..4284e804e2 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DataConsumerThroughputTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DataConsumerThroughputTests.cs @@ -18,7 +18,7 @@ public async Task MultipleDataConsumers_ShouldCompleteInReasonableTime(string tf TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); stopwatch.Stop(); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); Assert.IsLessThan(7, stopwatch.Elapsed.TotalSeconds, testHostResult.ToString()); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DiagnosticTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DiagnosticTests.cs index af24d00318..bf4b936ea8 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DiagnosticTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DiagnosticTests.cs @@ -73,7 +73,7 @@ public async Task Diag_WhenDiagnosticOutputFilePrefixButNotDiagnosticIsSpecified var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--diagnostic-file-prefix cccc", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'--diagnostic-file-prefix' requires '--diagnostic' to be provided"); } @@ -84,7 +84,7 @@ public async Task Diag_WhenDiagnosticOutputDirectoryButNotDiagnosticIsSpecified_ var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--diagnostic-output-directory cccc", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'--diagnostic-output-directory' requires '--diagnostic' to be provided"); } @@ -95,7 +95,7 @@ public async Task Diag_WhenDiagnosticFilePrefixAndDiagnosticOutputDirectoryButNo var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--diagnostic-file-prefix aaaa --diagnostic-output-directory cccc", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'--diagnostic-output-directory' requires '--diagnostic' to be provided"); } @@ -190,17 +190,17 @@ public async Task Diag_EnableWithEnvironmentVariables_Disable_Succeeded(string t { EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "0" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain("Diagnostic file"); testHostResult = await testHost.ExecuteAsync("--diagnostic", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputContains("Diagnostic file"); } private static async Task AssertDiagnosticReportWasGeneratedAsync(TestHostResult testHostResult, string diagPathPattern, string level = "Trace", string flushType = "async") { - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string outputPattern = $""" Diagnostic file \(level '{level}' with {flushType} flush\): {diagPathPattern} diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/EnvironmentVariablesConfigurationProviderTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/EnvironmentVariablesConfigurationProviderTests.cs index 0c5585c113..a8eed94d4c 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/EnvironmentVariablesConfigurationProviderTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/EnvironmentVariablesConfigurationProviderTests.cs @@ -14,7 +14,7 @@ public async Task SetEnvironmentVariable_ShouldSucceed(string currentTfm) { var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); } [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] @@ -28,14 +28,14 @@ public async Task TestHostMessesUpExitCode(string currentTfm) { ["MESS_UP_TESTHOST_EXIT_CODE"] = "1", }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult = await testHost.ExecuteAsync( environmentVariables: new() { ["MESS_UP_TESTHOST_EXIT_CODE"] = "100", }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult = await testHost.ExecuteAsync( environmentVariables: new() @@ -43,7 +43,7 @@ public async Task TestHostMessesUpExitCode(string currentTfm) ["MESS_UP_TESTHOST_EXIT_CODE"] = "1", ["ZERO_TESTS"] = "1", }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult = await testHost.ExecuteAsync( environmentVariables: new() @@ -51,7 +51,7 @@ public async Task TestHostMessesUpExitCode(string currentTfm) ["MESS_UP_TESTHOST_EXIT_CODE"] = "100", ["ZERO_TESTS"] = "1", }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult = await testHost.ExecuteAsync( environmentVariables: new() @@ -59,7 +59,7 @@ public async Task TestHostMessesUpExitCode(string currentTfm) ["MESS_UP_TESTHOST_EXIT_CODE"] = "8", ["ZERO_TESTS"] = "1", }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); } public sealed class TestAssetFixture() : TestAssetFixtureBase() diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionRequestCompleteTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionRequestCompleteTests.cs index 62af57f232..ba15b4e015 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionRequestCompleteTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionRequestCompleteTests.cs @@ -16,7 +16,7 @@ public async Task Exec_Honor_Request_Complete(string tfm) var stopwatch = Stopwatch.StartNew(); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); stopwatch.Stop(); - Assert.AreEqual(ExitCodes.Success, testHostResult.ExitCode); + testHostResult.AssertExitCodeIs(ExitCode.Success); Assert.IsGreaterThan(3, stopwatch.Elapsed.TotalSeconds); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs index 03a3c2d393..4fa04eff44 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ExecutionTests.cs @@ -15,7 +15,7 @@ public async Task Exec_WhenListTestsIsSpecified_AllTestsAreFound(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); const string OutputPattern = """ Test1 @@ -33,7 +33,7 @@ public async Task Exec_WhenOnlyAssetNameIsSpecified_AllTestsAreRun(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0); testHostResult.AssertOutputMatchesRegex($"Passed! - .*\\.(dll|exe) \\(net.+\\|.+\\)"); @@ -46,18 +46,18 @@ public async Task Exec_WhenUsingUidFilterForRun(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--filter-uid NonExistingUid", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult = await testHost.ExecuteAsync("--filter-uid 0", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); testHostResult = await testHost.ExecuteAsync("--filter-uid 1", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); testHostResult = await testHost.ExecuteAsync("--filter-uid 0 --filter-uid 1", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0); } @@ -68,24 +68,24 @@ public async Task Exec_WhenUsingUidFilterForDiscovery(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests --filter-uid NonExistingUid", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult = await testHost.ExecuteAsync("--list-tests --filter-uid 0", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputMatchesRegex(""" Test1 Test discovery summary: found 1 test\(s\) """); testHostResult = await testHost.ExecuteAsync("--list-tests --filter-uid 1", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputMatchesRegex(""" Test2 Test discovery summary: found 1 test\(s\) """); testHostResult = await testHost.ExecuteAsync("--list-tests --filter-uid 0 --filter-uid 1", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputMatchesRegex(""" Test1 Test2 @@ -100,7 +100,7 @@ public async Task Exec_WhenListTestsAndFilterAreSpecified_OnlyFilteredTestsAreFo var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests --treenode-filter \"\"", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); const string OutputPattern = """ Test1 @@ -117,7 +117,7 @@ public async Task Exec_WhenFilterIsSpecified_OnlyFilteredTestsAreRun(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--treenode-filter \"\"", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); testHostResult.AssertOutputMatchesRegex($"Passed! - .*\\.(dll|exe) \\(net.+\\|.+\\)"); @@ -130,7 +130,7 @@ public async Task Exec_WhenMinimumExpectedTestsIsSpecifiedAndEnoughTestsRun_Resu var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--minimum-expected-tests 2", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0); testHostResult.AssertOutputMatchesRegex($"Passed! - .*\\.(dll|exe) \\(net.+\\|.+\\)"); @@ -143,7 +143,7 @@ public async Task Exec_WhenMinimumExpectedTestsIsSpecifiedAndNotEnoughTestsRun_R var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--minimum-expected-tests 3", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.MinimumExpectedTestsPolicyViolation); + testHostResult.AssertExitCodeIs(ExitCode.MinimumExpectedTestsPolicyViolation); testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0, minimumNumberOfTests: 3); testHostResult.AssertOutputMatchesRegex($" - .*\\.(dll|exe) \\(net.+\\|.+\\)"); @@ -156,7 +156,7 @@ public async Task Exec_WhenListTestsAndMinimumExpectedTestsAreSpecified_Discover var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--list-tests --minimum-expected-tests 2", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Error: '--list-tests' and '--minimum-expected-tests' are incompatible options"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ForwardCompatibilityTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ForwardCompatibilityTests.cs index 6f9775b5a9..cdf0f16876 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ForwardCompatibilityTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ForwardCompatibilityTests.cs @@ -14,7 +14,7 @@ public async Task NewerPlatform_WithPreviousExtensions_ShouldExecuteTests() var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--crashdump --hangdump --report-trx --retry-failed-tests 3", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContainsSummary(0, 1, 0); string testResultsPath = Path.Combine(testHost.DirectoryName, "TestResults"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs index 137afff2cd..680ff98e0e 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs @@ -25,7 +25,7 @@ public async Task HangDump_Outputs_HangingTests_EvenWhenHangingTestsHaveTheSameD { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); testHostResult.AssertOutputContains("Test1"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpProcessTreeTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpProcessTreeTests.cs index 6a3f25a0ea..1ade122ee6 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpProcessTreeTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpProcessTreeTests.cs @@ -20,7 +20,7 @@ public async Task HangDump_DumpAllChildProcesses_CreateDump(string tfm) { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] dumpFiles = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories); Assert.HasCount(4, dumpFiles, $"There should be 4 dumps, one for each process in the tree. {testHostResult}"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpTests.cs index 7bb3db9d20..81137e9daf 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpTests.cs @@ -20,7 +20,7 @@ public async Task HangDump_DefaultSetting_CreateDump(string tfm) { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] dumpFiles = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories); Assert.ContainsSingle(dumpFiles, $"Expected single dump file. Found: {Environment.NewLine}{string.Join(Environment.NewLine, dumpFiles)}{Environment.NewLine}{testHostResult}"); } @@ -41,7 +41,7 @@ public async Task HangDump_WithDotnetTest_CreateDump() failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); - testResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] dumpFiles = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories); Assert.ContainsSingle(dumpFiles, $"Expected single dump file. Found: {Environment.NewLine}{string.Join(Environment.NewLine, dumpFiles)}{Environment.NewLine}{testResult}"); } @@ -62,7 +62,7 @@ public async Task HangDump_WithDotnetTest_NoHangButOverallTimeGreaterThanTimeout failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); - testResult.AssertExitCodeIs(ExitCodes.Success); + testResult.AssertExitCodeIs(ExitCode.Success); string[] dumpFiles = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories); Assert.IsEmpty(dumpFiles); } @@ -79,7 +79,7 @@ public async Task HangDump_CustomFileName_CreateDump() { "SLEEPTIMEMS1", "4000" }, { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string? dumpFile = Directory.GetFiles(resultDirectory, "myhungdumpfile_*.dmp", SearchOption.AllDirectories).SingleOrDefault(); Assert.IsNotNull(dumpFile, $"Dump file not found '{TargetFrameworks.NetCurrent}'\n{testHostResult}'"); } @@ -99,7 +99,7 @@ public async Task HangDump_PathWithSpaces_CreateDump() { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string? dumpFile = Directory.GetFiles(resultDirectory, "myhungdumpfile_*.dmp", SearchOption.AllDirectories).SingleOrDefault(); Assert.IsNotNull(dumpFile, $"Dump file not found '{TargetFrameworks.NetCurrent}'\n{testHostResult}'"); } @@ -122,7 +122,7 @@ public async Task HangDump_Formats_CreateDump(string format) { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string? dumpFile = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories).SingleOrDefault(); if (format != "None") @@ -148,7 +148,7 @@ public async Task HangDump_InvalidFormat_ShouldFail() { "SLEEPTIMEMS2", "600000" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains(""" Option '--hangdump-type' has invalid arguments: 'invalid' is not a valid dump type. Valid options are 'Mini', 'Heap', 'Triage', 'None' (only available in .NET 6+) and 'Full' @@ -169,7 +169,7 @@ public async Task HangDump_WithForegroundThreadAfterSessionFinish_CreateDump() { "SPAWN_FOREGROUND_THREAD", "true" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] dumpFiles = Directory.GetFiles(resultDirectory, "HangDump*.dmp", SearchOption.AllDirectories); Assert.ContainsSingle(dumpFiles, $"Expected single dump file. Found: {Environment.NewLine}{string.Join(Environment.NewLine, dumpFiles)}{Environment.NewLine}{testHostResult}"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs index 0869166317..5151f2f827 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs @@ -13,7 +13,7 @@ public async Task Help_WithAllExtensionsRegistered_OutputFullHelpContent(string var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.AllExtensionsTargetAssetPath, TestAssetFixture.AllExtensionsAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string wildcardPattern = $""" Microsoft.Testing.Platform v* @@ -120,7 +120,7 @@ public async Task HelpShortName_WithAllExtensionsRegistered_OutputFullHelpConten var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.AllExtensionsTargetAssetPath, TestAssetFixture.AllExtensionsAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("-?", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string wildcardPattern = $""" Microsoft.Testing.Platform v* @@ -139,7 +139,7 @@ public async Task Info_WithAllExtensionsRegistered_OutputFullInfoContent(string var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.AllExtensionsTargetAssetPath, TestAssetFixture.AllExtensionsAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--info", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string wildcardPattern = $""" Microsoft.Testing.Platform v* [*] diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs index 3a0a69655e..202fbc81b6 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs @@ -13,7 +13,7 @@ public async Task Help_WhenNoExtensionRegistered_OutputDefaultHelpContent(string var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.NoExtensionAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); const string wildcardMatchPattern = $""" Microsoft.Testing.Platform v* @@ -87,7 +87,7 @@ public async Task HelpShortName_WhenNoExtensionRegistered_OutputDefaultHelpConte var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.NoExtensionAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--?", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); const string wildcardMatchPattern = $""" Microsoft.Testing.Platform v* @@ -108,7 +108,7 @@ public async Task Help_WhenNoExtensionRegisteredAndUnknownOptionIsSpecified_Outp var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.NoExtensionAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"-{UnknownOption}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); const string wildcardMatchPattern = $""" Microsoft.Testing.Platform v* @@ -128,7 +128,7 @@ public async Task Info_WhenNoExtensionRegistered_OutputDefaultInfoContent(string var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.NoExtensionAssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--info", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string regexMatchPattern = $""" Microsoft.Testing.Platform v.+ \[.+\] @@ -301,7 +301,7 @@ public async Task Help_DoesNotCreateTestResultsFolder(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // Verify that TestResults folder was not created Assert.IsFalse(Directory.Exists(testResultsPath), "TestResults folder should not be created for help command"); @@ -323,7 +323,7 @@ public async Task HelpShortName_DoesNotCreateTestResultsFolder(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--?", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // Verify that TestResults folder was not created Assert.IsFalse(Directory.Exists(testResultsPath), "TestResults folder should not be created for help short name command"); @@ -345,7 +345,7 @@ public async Task Info_DoesNotCreateTestResultsFolder(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--info", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // Verify that TestResults folder was not created Assert.IsFalse(Directory.Exists(testResultsPath), "TestResults folder should not be created for info command"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceAssert.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceAssert.cs index 88973c7c5d..8f5c8cf79c 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceAssert.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceAssert.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.Testing.Platform.Helpers; + namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; internal static class AcceptanceAssert @@ -8,9 +10,15 @@ internal static class AcceptanceAssert public static void AssertExitCodeIs(this TestHostResult testHostResult, int exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) => Assert.AreEqual(exitCode, testHostResult.ExitCode, GenerateFailedAssertionMessage(testHostResult, callerMemberName: callerMemberName, callerFilePath: callerFilePath, callerLineNumber: callerLineNumber)); + public static void AssertExitCodeIs(this TestHostResult testHostResult, ExitCode exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + => AssertExitCodeIs(testHostResult, (int)exitCode, callerMemberName, callerFilePath, callerLineNumber); + public static void AssertExitCodeIsNot(this TestHostResult testHostResult, int exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) => Assert.AreNotEqual(exitCode, testHostResult.ExitCode, GenerateFailedAssertionMessage(testHostResult, callerMemberName: callerMemberName, callerFilePath: callerFilePath, callerLineNumber: callerLineNumber)); + public static void AssertExitCodeIsNot(this TestHostResult testHostResult, ExitCode exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + => AssertExitCodeIsNot(testHostResult, (int)exitCode, callerMemberName, callerFilePath, callerLineNumber); + /// /// Ensure that the output matches the given pattern. The pattern can use `*` to mean any character, it is internally replaced by `.*` and matched as regex. /// If you have lines in the pattern that are optional then you can output `###SKIP###` and the line in pattern will be skipped. This allows matching lines that are present only when some condition is met. @@ -90,9 +98,15 @@ public static void AssertOutputContains(this TestHostResult testHostResult, stri public static void AssertExitCodeIs(this DotnetMuxerResult testHostResult, int exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) => Assert.AreEqual(exitCode, testHostResult.ExitCode, GenerateFailedAssertionMessage(testHostResult, callerMemberName: callerMemberName, callerFilePath: callerFilePath, callerLineNumber: callerLineNumber)); + public static void AssertExitCodeIs(this DotnetMuxerResult testHostResult, ExitCode exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + => AssertExitCodeIs(testHostResult, (int)exitCode, callerMemberName, callerFilePath, callerLineNumber); + public static void AssertExitCodeIsNot(this DotnetMuxerResult testHostResult, int exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) => Assert.AreNotEqual(exitCode, testHostResult.ExitCode, GenerateFailedAssertionMessage(testHostResult, callerMemberName: callerMemberName, callerFilePath: callerFilePath, callerLineNumber: callerLineNumber)); + public static void AssertExitCodeIsNot(this DotnetMuxerResult testHostResult, ExitCode exitCode, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + => AssertExitCodeIsNot(testHostResult, (int)exitCode, callerMemberName, callerFilePath, callerLineNumber); + public static void AssertOutputContains(this DotnetMuxerResult dotnetMuxerResult, string value, [CallerMemberName] string? callerMemberName = null, [CallerFilePath] string? callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) => Assert.Contains(value, dotnetMuxerResult.StandardOutput, StringComparison.Ordinal, GenerateFailedAssertionMessage(dotnetMuxerResult, callerMemberName: callerMemberName, callerFilePath: callerFilePath, callerLineNumber: callerLineNumber)); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationFailingTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationFailingTests.cs index d36239551b..690bd3f91b 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationFailingTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationFailingTests.cs @@ -36,7 +36,7 @@ public async Task Execution_WithFailingTest_OutputContainsTranslatedFailureSumma environmentVariables: new() { ["DOTNET_CLI_UI_LANGUAGE"] = "fr" }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); // Verify failure summary is in French ("Résumé de série de tests : Échec!") AssertOutputContainsNormalized(testHostResult, "Résumé de série de tests : Échec!"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationTests.cs index 267fb8467d..4e0fcd51c3 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/LocalizationTests.cs @@ -45,7 +45,7 @@ public async Task Execution_WithFrenchLocale_OutputContainsTranslatedSummary(str environmentVariables: new() { ["DOTNET_CLI_UI_LANGUAGE"] = "fr" }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // Verify the summary line is in French ("Résumé de série de tests : Réussite!") AssertOutputContainsNormalized(testHostResult, "Résumé de série de tests : Réussite!"); @@ -70,7 +70,7 @@ public async Task Execution_WithSpanishLocale_OutputContainsTranslatedSummary(st environmentVariables: new() { ["DOTNET_CLI_UI_LANGUAGE"] = "es" }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // Verify the summary line is in Spanish ("Resumen de la serie de pruebas: Correcta!") testHostResult.AssertOutputContains("Resumen de la serie de pruebas: Correcta!"); @@ -99,7 +99,7 @@ public async Task Execution_WithTestingPlatformUILanguage_TakesPrecedenceOverDot }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); // French should win because TESTINGPLATFORM_UI_LANGUAGE has higher precedence AssertOutputContainsNormalized(testHostResult, "Résumé de série de tests : Réussite!"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuildTests.GenerateEntryPoint.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuildTests.GenerateEntryPoint.cs index dfb13db03f..2a63dc2d5e 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuildTests.GenerateEntryPoint.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuildTests.GenerateEntryPoint.cs @@ -145,7 +145,7 @@ private async Task GenerateAndVerifyLanguageSpecificEntryPointAsync(string asset var testHost = TestInfrastructure.TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, tfm, rid: RID, verb: verb, buildConfiguration: compilationMode); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); Assert.Contains("Passed!", testHostResult.StandardOutput); SL.Target[] coreCompileTargets = binLog.FindChildrenRecursive().Where(t => t.Name == "CoreCompile" && t.Children.Count > 0).ToArray(); @@ -171,7 +171,7 @@ private async Task GenerateAndVerifyLanguageSpecificEntryPointAsync(string asset testHost = TestInfrastructure.TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, tfm, rid: RID, verb: verb, buildConfiguration: compilationMode); testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - Assert.AreEqual(ExitCodes.Success, testHostResult.ExitCode); + testHostResult.AssertExitCodeIs(ExitCode.Success); Assert.Contains("Passed!", testHostResult.StandardOutput); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs index a58aaed423..a1e4569add 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MaxFailedTestsExtensionTests.cs @@ -14,7 +14,7 @@ public async Task TestMaxFailedTestsShouldCallStopTestExecutionAsync() var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync("--maximum-failed-tests 2", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestExecutionStoppedForMaxFailedTests); + testHostResult.AssertExitCodeIs(ExitCode.TestExecutionStoppedForMaxFailedTests); testHostResult.AssertOutputContains("Test session is aborting due to reaching failures ('2') specified by the '--maximum-failed-tests' option."); testHostResult.AssertOutputContainsSummary(failed: 3, passed: 3, skipped: 0); @@ -32,7 +32,7 @@ public async Task WhenCapabilityIsMissingShouldFail() }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("The current test framework does not implement 'IGracefulStopTestExecutionCapability' which is required for '--maximum-failed-tests' feature."); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/NoBannerTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/NoBannerTests.cs index 9af14600dd..9e4a6e2207 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/NoBannerTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/NoBannerTests.cs @@ -16,7 +16,7 @@ public async Task UsingNoBanner_TheBannerDoesNotAppear(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--no-banner", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotMatchRegex(_bannerRegexMatchPattern); } @@ -33,7 +33,7 @@ public async Task UsingNoBanner_InTheEnvironmentVars_TheBannerDoesNotAppear(stri }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotMatchRegex(_bannerRegexMatchPattern); } @@ -50,7 +50,7 @@ public async Task UsingDotnetNoLogo_InTheEnvironmentVars_TheBannerDoesNotAppear( }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotMatchRegex(_bannerRegexMatchPattern); } @@ -61,7 +61,7 @@ public async Task WithoutUsingNoBanner_TheBannerAppears(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputMatchesRegex(_bannerRegexMatchPattern); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/RetryFailedTestsTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/RetryFailedTestsTests.cs index 59a143d47f..b53aa9ab19 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/RetryFailedTestsTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/RetryFailedTestsTests.cs @@ -40,7 +40,7 @@ public async Task RetryFailedTests_OnlyRetryTimes_Succeeds(string tfm, bool fail if (!failOnly) { - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Tests suite completed successfully in 2 attempts"); testHostResult.AssertOutputContains("Failed! -"); testHostResult.AssertOutputContains("Passed! -"); @@ -56,7 +56,7 @@ public async Task RetryFailedTests_OnlyRetryTimes_Succeeds(string tfm, bool fail } else { - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Tests suite failed in all 4 attempts"); testHostResult.AssertOutputContains("Tests suite failed, total failed tests: 1, exit code: 2, attempt: 1/4"); testHostResult.AssertOutputContains("Tests suite failed, total failed tests: 1, exit code: 2, attempt: 2/4"); @@ -96,14 +96,14 @@ public async Task RetryFailedTests_MaxPercentage_Succeeds(string tfm, bool fail) if (fail) { - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Failure threshold policy is enabled, failed tests will not be restarted."); testHostResult.AssertOutputContains("Percentage failed threshold is 50% and 66.67% tests failed (2/3)"); testHostResult.AssertOutputContains("Failed! -"); } else { - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Tests suite completed successfully in 2 attempts"); testHostResult.AssertOutputContains("Failed! -"); testHostResult.AssertOutputContains("Passed! -"); @@ -128,14 +128,14 @@ public async Task RetryFailedTests_MaxTestsCount_Succeeds(string tfm, bool fail) if (fail) { - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputContains("Failure threshold policy is enabled, failed tests will not be restarted."); testHostResult.AssertOutputContains("Maximum failed tests threshold is 1 and 2 tests failed"); testHostResult.AssertOutputContains("Failed! -"); } else { - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Tests suite completed successfully in 2 attempts"); testHostResult.AssertOutputContains("Failed! -"); testHostResult.AssertOutputContains("Passed! -"); @@ -159,7 +159,7 @@ public async Task RetryFailedTests_MoveFiles_Succeeds(string tfm) }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] entries = [.. Directory.GetFiles(resultDirectory, "*.*", SearchOption.AllDirectories).Where(x => !x.Contains("Retries", StringComparison.OrdinalIgnoreCase))]; @@ -189,7 +189,7 @@ public async Task RetryFailedTests_PassingFromFirstTime_UsingTestTarget_MoveFile $"build \"{AssetFixture.TargetAssetPath}\" -c Release -t:DispatchToInnerBuildsWithMTPTestTarget -p:TestingPlatformCommandLineArguments=\"--retry-failed-tests 1 --results-directory %22{resultDirectory}%22\"", workingDirectory: AssetFixture.TargetAssetPath, cancellationToken: TestContext.CancellationToken); - result.AssertExitCodeIs(ExitCodes.Success); + result.AssertExitCodeIs(ExitCode.Success); // File names are on the form: RetryFailedTests_tfm_architecture.log string[] logFilesFromInvokeTestingPlatformTask = Directory.GetFiles(resultDirectory, "RetryFailedTests_*_*.log"); @@ -223,7 +223,7 @@ public async Task RetryFailedTests_WithPreexistingFilterUid_ReplacesFilterOnRetr }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("Tests suite completed successfully in 2 attempts"); testHostResult.AssertOutputContains("Tests suite failed, total failed tests: 1, exit code: 2, attempt: 1/4"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryDisabledTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryDisabledTests.cs index b832ade98c..a025b50d32 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryDisabledTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryDisabledTests.cs @@ -20,7 +20,7 @@ public async Task Telemetry_WhenEnableTelemetryIsFalse_WithTestApplicationOption var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--diagnostic", disableTelemetry: false, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string diagContentsPattern = """ @@ -34,7 +34,7 @@ public async Task Telemetry_WhenEnableTelemetryIsFalse_WithTestApplicationOption private static async Task AssertDiagnosticReportAsync(TestHostResult testHostResult, string diagPathPattern, string diagContentsPattern, string level = "Trace", string flushType = "async") { - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string outputPattern = $""" Diagnostic file \(level '{level}' with {flushType} flush\): {diagPathPattern} diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryTests.cs index ceec796581..b75d2d8914 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TelemetryTests.cs @@ -20,7 +20,7 @@ public async Task Telemetry_ByDefault_TelemetryIsEnabled(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--diagnostic", disableTelemetry: false, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string diagContentsPattern = """ @@ -48,7 +48,7 @@ public async Task Telemetry_WhenOptingOutTelemetry_WithEnvironmentVariable_Telem }, disableTelemetry: false, TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string diagContentsPattern = """ @@ -76,7 +76,7 @@ public async Task Telemetry_WhenOptingOutTelemetry_With_DOTNET_CLI_EnvironmentVa }, disableTelemetry: false, TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string diagContentsPattern = """ @@ -90,7 +90,7 @@ public async Task Telemetry_WhenOptingOutTelemetry_With_DOTNET_CLI_EnvironmentVa private static async Task AssertDiagnosticReportAsync(TestHostResult testHostResult, string diagPathPattern, string diagContentsPattern, string level = "Trace", string flushType = "async") { - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string outputPattern = $""" Diagnostic file \(level '{level}' with {flushType} flush\): {diagPathPattern} diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostProcessLifetimeHandlerTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostProcessLifetimeHandlerTests.cs index 3b352e224d..f8b9b98a40 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostProcessLifetimeHandlerTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TestHostProcessLifetimeHandlerTests.cs @@ -14,7 +14,7 @@ public async Task All_Interface_Methods_ShouldBe_Invoked(string currentTfm) { var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); Assert.AreEqual("TestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync", File.ReadAllText(Path.Combine(testHost.DirectoryName, "BeforeTestHostProcessStartAsync.txt"))); Assert.AreEqual("TestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync", File.ReadAllText(Path.Combine(testHost.DirectoryName, "OnTestHostProcessStartedAsync.txt"))); Assert.AreEqual("TestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync", File.ReadAllText(Path.Combine(testHost.DirectoryName, "OnTestHostProcessExitedAsync.txt"))); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TimeoutTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TimeoutTests.cs index 8c4005a3a5..5bc3d61ad0 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TimeoutTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TimeoutTests.cs @@ -13,7 +13,7 @@ public async Task TimeoutWithInvalidArg_WithoutLetterSuffix_OutputInvalidMessage var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -24,7 +24,7 @@ public async Task TimeoutWithInvalidArg_WithInvalidLetterSuffix_OutputInvalidMes var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5y", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -35,7 +35,7 @@ public async Task TimeoutWithInvalidArg_WithInvalidFormat_OutputInvalidMessage(s var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 5h6m", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("'timeout' option should have one argument as string in the format [h|m|s] where 'value' is float"); } @@ -46,7 +46,7 @@ public async Task TimeoutWithValidArg_WithTestTimeOut_OutputContainsCancelingMes var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 1s", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("Canceling the test session"); } @@ -57,7 +57,7 @@ public async Task TimeoutWithValidArg_WithSecondAsSuffix_WithTestNotTimeOut_Outp var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 12.5s", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain("Canceling the test session"); } @@ -68,7 +68,7 @@ public async Task TimeoutWithValidArg_WithMinuteAsSuffix_WithTestNotTimeOut_Outp var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 1m", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain("Canceling the test session"); } @@ -79,7 +79,7 @@ public async Task TimeoutWithValidArg_WithHourAsSuffix_WithTestNotTimeOut_Output var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.NoExtensionTargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--timeout 1h", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); testHostResult.AssertOutputDoesNotContain("Canceling the test session"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxDataRowTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxDataRowTests.cs index 3353c81700..fe6e9ea2ea 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxDataRowTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxDataRowTests.cs @@ -22,7 +22,7 @@ public async Task Trx_WhenTheTestNameHasInvalidXmlChar_TheTrxCreatedSuccessfully private async Task AssertTrxReportWasGeneratedAsync(TestHostResult testHostResult, string trxPathPattern, int numberOfTests) { - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string outputPattern = $""" In process file artifacts produced: diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxFailingTestTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxFailingTestTests.cs index 88e93378ba..b9c2fdfde5 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxFailingTestTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxFailingTestTests.cs @@ -14,7 +14,7 @@ public async Task Trx_WhenTestFails_ContainsExceptionInfoInOutput(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {fileName}.trx", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); string[] trxFiles = Directory.GetFiles(testHost.DirectoryName, $"{fileName}.trx", SearchOption.AllDirectories); Assert.HasCount(1, trxFiles, $"Expected exactly one trx file but found {trxFiles.Length}: {string.Join(", ", trxFiles)}"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxSkippedTestTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxSkippedTestTests.cs index 49ae0774a3..6c5b15b27b 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxSkippedTestTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxSkippedTestTests.cs @@ -14,7 +14,7 @@ public async Task Trx_WhenSkipTest_ItAppearsAsExpectedInsideTheTrx(string tfm) var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetNameUsingMSTest, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {fileName}.trx", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string[] trxFiles = Directory.GetFiles(testHost.DirectoryName, $"{fileName}.trx", SearchOption.AllDirectories); Assert.HasCount(1, trxFiles, $"Expected exactly one trx file but found {trxFiles.Length}: {string.Join(", ", trxFiles)}"); @@ -37,7 +37,7 @@ public async Task Trx_UsingDataDriven_CreatesUnitTestTagForEachOneInsideTheTrx(s var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetNameUsingMSTest, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {fileName}.trx", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.ZeroTests); + testHostResult.AssertExitCodeIs(ExitCode.ZeroTests); string[] trxFiles = Directory.GetFiles(testHost.DirectoryName, $"{fileName}.trx", SearchOption.AllDirectories); Assert.HasCount(1, trxFiles, $"Expected exactly one trx file but found {trxFiles.Length}: {string.Join(", ", trxFiles)}"); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxTests.cs index fadc3dac8f..2bb019b8b1 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TrxTests.cs @@ -13,7 +13,7 @@ public async Task Trx_WhenReportTrxIsNotSpecified_TrxReportIsNotGenerated(string var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string outputPattern = """ Out of process file artifacts produced: @@ -46,7 +46,7 @@ public async Task Trx_WhenTestHostCrash_ErrorIsDisplayedInsideTheTrx(string tfm) $"--crashdump --report-trx --report-trx-filename {fileName}.trx", new() { { "CRASHPROCESS", "1" } }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] trxFiles = Directory.GetFiles(testHost.DirectoryName, $"{fileName}.trx", SearchOption.AllDirectories); Assert.HasCount(1, trxFiles, $"Expected exactly one trx file but found {trxFiles.Length}: {string.Join(", ", trxFiles)}"); @@ -70,7 +70,7 @@ public async Task Trx_WhenTestHostCrash_RunningUnderDotnetTest_ErrorIsDisplayedI failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); - result.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + result.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); string[] trxFiles = Directory.GetFiles(testResultsPath, $"{fileName}.trx", SearchOption.AllDirectories); Assert.HasCount(1, trxFiles, $"Expected exactly one trx file but found {trxFiles.Length}: {string.Join(", ", trxFiles)}"); @@ -90,7 +90,7 @@ public async Task Trx_WhenReportTrxIsSpecifiedWithFullPath_TrxReportShouldFail(s var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {Path.Combine(testResultsPath, "report.trx")}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Option '--report-trx-filename' has invalid arguments: file name argument must not contain path (e.g. --report-trx-filename myreport.trx)"); } @@ -101,7 +101,7 @@ public async Task Trx_WhenReportTrxIsSpecifiedWithRelativePath_TrxReportShouldFa var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {Path.Combine("aaa", "report.trx")}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Option '--report-trx-filename' has invalid arguments: file name argument must not contain path (e.g. --report-trx-filename myreport.trx)"); } @@ -112,7 +112,7 @@ public async Task Trx_WhenReportTrxIsNotSpecifiedAndReportTrxPathIsSpecified_Err var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--report-trx-filename report.trx", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Error: '--report-trx-filename' requires '--report-trx' to be enabled"); } @@ -124,13 +124,13 @@ public async Task Trx_WhenReportTrxIsSpecifiedAndReportTrxPathIsSpecified_Overwr string reportFileName = $"report-{tfm}.trx"; TestHostResult testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {reportFileName}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string warningMessage = $"Warning: Trx file '{Path.Combine(testHost.DirectoryName, "TestResults", reportFileName)}' already exists and will be overwritten."; testHostResult.AssertOutputDoesNotContain(warningMessage); testHostResult = await testHost.ExecuteAsync($"--report-trx --report-trx-filename {reportFileName}", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains(warningMessage); } @@ -141,13 +141,13 @@ public async Task Trx_WhenReportTrxIsSpecifiedAndListTestsIsSpecified_ErrorIsDis var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.AssetName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync("--report-trx --list-tests", cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.InvalidCommandLine); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); testHostResult.AssertOutputContains("Error: '--report-trx' cannot be enabled when using '--list-tests'"); } private async Task AssertTrxReportWasGeneratedAsync(TestHostResult testHostResult, string trxPathPattern, int numberOfTests) { - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); string outputPattern = $""" In process file artifacts produced: diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TypeForwardingTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TypeForwardingTests.cs index 67aa81c055..1f72999650 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TypeForwardingTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/TypeForwardingTests.cs @@ -73,7 +73,7 @@ public async Task SettingDisplayNameFromNetStandardLibraryDuringNetCurrentRuntim var testHost = TestInfrastructure.TestHost.LocateFrom($"{testAsset.TargetAssetPath}/ConsoleApp", "ConsoleApp", TargetFrameworks.NetCurrent); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputContains("MyDisplayName"); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/UnhandledExceptionPolicyTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/UnhandledExceptionPolicyTests.cs index 1447ca364f..5ffe202b9d 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/UnhandledExceptionPolicyTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/UnhandledExceptionPolicyTests.cs @@ -46,19 +46,19 @@ public async Task UnhandledExceptionPolicy_ConfigFile_UnobservedTaskException_Sh case Mode.Enabled: File.WriteAllText(configFileName, contentFile.Replace("\"exitProcessOnUnhandledException\": false", "\"exitProcessOnUnhandledException\": true")); testHostResult = await testHost.ExecuteAsync(null, new() { { "UNOBSERVEDTASKEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("[UnhandledExceptionHandler.OnTaskSchedulerUnobservedTaskException(testhost controller workflow)]"); break; case Mode.Disabled: File.WriteAllText(configFileName, contentFile.Replace("\"exitProcessOnUnhandledException\": false", "\"exitProcessOnUnhandledException\": false")); testHostResult = await testHost.ExecuteAsync(null, new() { { "UNOBSERVEDTASKEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputDoesNotContain("[UnhandledExceptionHandler.OnTaskSchedulerUnobservedTaskException]"); break; case Mode.Default: File.Delete(configFileName); testHostResult = await testHost.ExecuteAsync(null, new() { { "UNOBSERVEDTASKEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputDoesNotContain("[UnhandledExceptionHandler.OnTaskSchedulerUnobservedTaskException]"); break; case Mode.DisabledByEnvironmentVariable: @@ -71,7 +71,7 @@ public async Task UnhandledExceptionPolicy_ConfigFile_UnobservedTaskException_Sh { EnvironmentVariableConstants.TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION, "0" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertExitCodeIs(ExitCode.Success); testHostResult.AssertOutputDoesNotContain("[UnhandledExceptionHandler.OnTaskSchedulerUnobservedTaskException]"); break; case Mode.EnabledByEnvironmentVariable: @@ -84,7 +84,7 @@ public async Task UnhandledExceptionPolicy_ConfigFile_UnobservedTaskException_Sh { EnvironmentVariableConstants.TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION, "1" }, }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); testHostResult.AssertOutputContains("[UnhandledExceptionHandler.OnTaskSchedulerUnobservedTaskException(testhost controller workflow)]"); break; default: @@ -111,18 +111,18 @@ public async Task UnhandledExceptionPolicy_EnvironmentVariable_UnhandledExceptio testHostResult = await testHost.ExecuteAsync(null, new() { { "UNHANDLEDEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContains("[UnhandledExceptionHandler.OnCurrentDomainUnhandledException(testhost controller workflow)]"); testHostResult.AssertOutputContains("IsTerminating: True"); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); break; case Mode.Disabled: File.WriteAllText(configFileName, contentFile.Replace("\"exitProcessOnUnhandledException\": false", "\"exitProcessOnUnhandledException\": false")); testHostResult = await testHost.ExecuteAsync(null, new() { { "UNHANDLEDEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); Assert.IsTrue(testHostResult.StandardError.Contains("Unhandled exception", StringComparison.OrdinalIgnoreCase), testHostResult.ToString()); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); break; case Mode.Default: File.Delete(configFileName); testHostResult = await testHost.ExecuteAsync(null, new() { { "UNHANDLEDEXCEPTION", "1" } }, cancellationToken: TestContext.CancellationToken); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); break; case Mode.DisabledByEnvironmentVariable: File.WriteAllText(configFileName, contentFile.Replace("\"exitProcessOnUnhandledException\": false", "\"exitProcessOnUnhandledException\": true")); @@ -136,7 +136,7 @@ public async Task UnhandledExceptionPolicy_EnvironmentVariable_UnhandledExceptio cancellationToken: TestContext.CancellationToken); Assert.IsTrue(testHostResult.StandardError.Contains("Unhandled exception", StringComparison.OrdinalIgnoreCase), testHostResult.ToString()); testHostResult.AssertOutputDoesNotContain("IsTerminating: True"); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); break; case Mode.EnabledByEnvironmentVariable: File.WriteAllText(configFileName, contentFile.Replace("\"exitProcessOnUnhandledException\": false", "\"exitProcessOnUnhandledException\": false")); @@ -150,7 +150,7 @@ public async Task UnhandledExceptionPolicy_EnvironmentVariable_UnhandledExceptio cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContains("[UnhandledExceptionHandler.OnCurrentDomainUnhandledException(testhost controller workflow)]"); testHostResult.AssertOutputContains("IsTerminating: True"); - testHostResult.AssertExitCodeIsNot(ExitCodes.Success); + testHostResult.AssertExitCodeIsNot(ExitCode.Success); break; default: throw new NotImplementedException($"Mode not found '{mode}'"); diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index 677c1d7cf5..4ed0c077a5 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -11,7 +11,7 @@ namespace MSTest.Analyzers.Test; public sealed class DoNotUseSystemDescriptionAttributeAnalyzerTests { [TestMethod] - public async Task WhenTestMethodHasSystemDescriptionAttribute_Diagnostic() + public async Task WhenTestMethodHasFullyQualifiedSystemDescriptionAttribute_Diagnostic() { string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -44,6 +44,72 @@ public void MyTestMethod() await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } + [TestMethod] + public async Task WhenTestMethodHasSystemDescriptionAttributeWithSystemComponentModelUsing_UsesFullyQualifiedMSTestDescription() + { + string code = """ + using System.ComponentModel; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [Description("Description")] + public void [|MyTestMethod|]() + { + } + } + """; + + string fixedCode = """ + using System.ComponentModel; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenTestMethodHasSystemDescriptionAttributeWithoutUnitTestingUsing_UsesFullyQualifiedMSTestDescription() + { + string code = """ + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] + public class MyTestClass + { + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + [System.ComponentModel.Description("Description")] + public void [|MyTestMethod|]() + { + } + } + """; + + string fixedCode = """ + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] + public class MyTestClass + { + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + [TestMethod] public async Task WhenMethodWithoutTestMethodAttribute_HasSystemDescriptionAttribute_NoDiagnostic() { diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DuplicateDataRowAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DuplicateDataRowAnalyzerTests.cs index 49c9de6b33..6e2d7c31ae 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DuplicateDataRowAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DuplicateDataRowAnalyzerTests.cs @@ -3,7 +3,7 @@ using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< MSTest.Analyzers.DuplicateDataRowAnalyzer, - Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + MSTest.Analyzers.DuplicateDataRowFixer>; namespace MSTest.Analyzers.Test; @@ -289,4 +289,117 @@ public static void TestMethod4(float x) await VerifyCS.VerifyAnalyzerAsync(code); } + + [TestMethod] + public async Task WhenDuplicateDataRow_CodeFixRemovesDuplicate() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow(5)] + [[|DataRow(5)|]] + public static void TestMethod(int x) + { + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow(5)] + public static void TestMethod(int x) + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenDuplicateDataRowParameterless_CodeFixRemovesDuplicate() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow] + [[|DataRow|]] + public static void TestMethod() + { + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow] + public static void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenMultipleDuplicateDataRows_CodeFixRemovesEach() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow(1, 2)] + [[|DataRow(1, 2)|]] + [[|DataRow(1, 2)|]] + public static void TestMethod(int x, int y) + { + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [DataRow(1, 2)] + public static void TestMethod(int x, int y) + { + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = fixedCode, + NumberOfFixAllIterations = 2, + NumberOfFixAllInDocumentIterations = 2, + NumberOfFixAllInProjectIterations = 2, + }.RunAsync(); + } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/TestMethodShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/TestMethodShouldBeValidAnalyzerTests.cs index b28d976280..d9dbbcaa8d 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/TestMethodShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/TestMethodShouldBeValidAnalyzerTests.cs @@ -3,7 +3,7 @@ using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< MSTest.Analyzers.TestMethodShouldBeValidAnalyzer, - MSTest.Analyzers.TestMethodShouldBeValidCodeFixProvider>; + MSTest.Analyzers.TestMethodShouldBeValidFixer>; namespace MSTest.Analyzers.Test; diff --git a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs index 0676c3dd5f..926e344146 100644 --- a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs +++ b/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs @@ -245,6 +245,39 @@ await visitor.VisitAsync((testNode, parentNodeUid) => Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[6].Node.GetType()); } + [TestMethod] + public async Task Visit_WhenFilterHasPropertyExpression_OnlyIncludesNodesMatchingProperty() + { + // Arrange — filter with a property expression (ContainsPropertyFilters == true) + var nodeWithMatchingTag = new TestNode + { + StableUid = "ID1", + DisplayName = "A", + Properties = [new TestMetadataProperty("Tag", "Fast")], + }; + var nodeWithNonMatchingTag = new TestNode + { + StableUid = "ID2", + DisplayName = "A", + Properties = [new TestMetadataProperty("Tag", "Slow")], + }; + + var filter = new TreeNodeFilter("/A[Tag=Fast]"); + var visitor = new BFSTestNodeVisitor(new[] { nodeWithMatchingTag, nodeWithNonMatchingTag }, filter, null!); + + // Act + List includedTestNodes = []; + await visitor.VisitAsync((testNode, _) => + { + includedTestNodes.Add(testNode); + return Task.CompletedTask; + }); + + // Assert + Assert.HasCount(1, includedTestNodes); + Assert.AreEqual("ID1", includedTestNodes[0].StableUid); + } + private static TestNode CreateParameterizedTestNode(string parameterizedTestNode, bool? expansionPropertyValue) { TestNode rootNode = parameterizedTestNode switch diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs index 98e17cfcb3..e2d249bdea 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs @@ -1501,6 +1501,45 @@ public void ResolveArgumentsShouldReturnPopulatedParamsWithAllProvided() ((string[])expectedArguments[1]).SequenceEqual((string[])resolvedArguments[1]!).Should().BeTrue(); } + // Regression tests for https://github.com/microsoft/testfx/issues/7846 + // Verify that log output buffers are cleared between invocations to prevent + // exponential memory growth with DynamicData tests. + // NOTE: The TestClassInfo (class init/cleanup) and UnitTestRunner (assembly init/cleanup) + // call sites use the same GetAndClear* methods tested in isolation in + // TestContextImplementationTests.GetAndClear{Output,Error,Trace}_ShouldReturnContentThenClearBuffer. + public async Task InvokeAsync_ShouldNotAccumulateLogOutputAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteConsoleOut("invocation_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.LogOutput.Should().Be("invocation_output"); + result2.LogOutput.Should().Be("invocation_output"); + } + + public async Task InvokeAsync_ShouldNotAccumulateLogErrorAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteConsoleErr("error_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.LogError.Should().Be("error_output"); + result2.LogError.Should().Be("error_output"); + } + + public async Task InvokeAsync_ShouldNotAccumulateDebugTraceAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteTrace("trace_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.DebugTrace.Should().Be("trace_output"); + result2.DebugTrace.Should().Be("trace_output"); + } + #region helper methods private static async Task RunWithTestablePlatformService(TestablePlatformServiceProvider testablePlatformServiceProvider, Func action) diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs index 1c900fae88..a0ad10507b 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs @@ -347,6 +347,42 @@ public void DisplayMessageShouldForwardToIMessageLogger() messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Error, "ErrorMessage"), Times.Once); } + public void GetAndClearOutput_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteConsoleOut("hello"); + + string? first = _testContextImplementation.GetAndClearOutput(); + string? second = _testContextImplementation.GetAndClearOutput(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + + public void GetAndClearError_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteConsoleErr("hello"); + + string? first = _testContextImplementation.GetAndClearError(); + string? second = _testContextImplementation.GetAndClearError(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + + public void GetAndClearTrace_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteTrace("hello"); + + string? first = _testContextImplementation.GetAndClearTrace(); + string? second = _testContextImplementation.GetAndClearTrace(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + public void WritesFromBackgroundThreadShouldNotThrow() { TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); @@ -360,8 +396,9 @@ public void WritesFromBackgroundThreadShouldNotThrow() }); t.Start(); - _ = testContextImplementation.GetOut(); - _ = testContextImplementation.GetErr(); + _ = testContextImplementation.GetAndClearOutput(); + _ = testContextImplementation.GetAndClearError(); + _ = testContextImplementation.GetAndClearTrace(); t.Join(); } } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/PasteArgumentsTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/PasteArgumentsTests.cs new file mode 100644 index 0000000000..bdc271bccb --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/PasteArgumentsTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.UnitTests; + +[TestClass] +public sealed class PasteArgumentsTests +{ + // Test cases mirror https://github.com/dotnet/runtime/blob/3ac6e13b2780bbaf03c62488fced60b9a76e9782/src/libraries/Common/tests/Tests/System/PasteArgumentsTests.cs + [DataRow("app.exe arg1 arg2", "app.exe", "arg1", "arg2")] + [DataRow("\"app name.exe\" arg1 arg2", "app name.exe", "arg1", "arg2")] + [DataRow("app.exe \\\\ arg2", "app.exe", "\\\\", "arg2")] + [DataRow("app.exe \"\\\"\" arg2", "app.exe", "\"", "arg2")] // literal double quotation mark character + [DataRow("app.exe \"\\\\\\\"\" arg2", "app.exe", "\\\"", "arg2")] // 2N+1 backslashes before quote rule + [DataRow("app.exe \"\\\\\\\\\\\"\" arg2", "app.exe", "\\\\\"", "arg2")] // 2N backslashes before quote rule + [TestMethod] + public void Pastes(string expected, string arg0, string arg1, string arg2) + { + var sb = new StringBuilder(); + PasteArguments.AppendArgument(sb, arg0); + PasteArguments.AppendArgument(sb, arg1); + PasteArguments.AppendArgument(sb, arg2); + Assert.AreEqual(expected, sb.ToString()); + } +} diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Logging/LoggerFactoryProxyTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Logging/LoggerFactoryProxyTests.cs new file mode 100644 index 0000000000..43abce1894 --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Logging/LoggerFactoryProxyTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Logging; + +using Moq; + +namespace Microsoft.Testing.Platform.UnitTests; + +[TestClass] +public sealed class LoggerFactoryProxyTests +{ + [TestMethod] + public void CreateLogger_WhenNotInitialized_ThrowsInvalidOperationException() + { + LoggerFactoryProxy proxy = new(); + + Assert.ThrowsExactly(() => proxy.CreateLogger("test")); + } + + [TestMethod] + public void SetLoggerFactory_WithNull_ThrowsArgumentNullException() + { + LoggerFactoryProxy proxy = new(); + + Assert.ThrowsExactly(() => proxy.SetLoggerFactory(null!)); + } + + [TestMethod] + public void CreateLogger_WhenInitialized_DelegatesToInnerFactory() + { + Mock mockLogger = new(); + Mock mockFactory = new(); + mockFactory.Setup(f => f.CreateLogger("category")).Returns(mockLogger.Object); + + LoggerFactoryProxy proxy = new(); + proxy.SetLoggerFactory(mockFactory.Object); + + ILogger result = proxy.CreateLogger("category"); + + Assert.AreSame(mockLogger.Object, result); + mockFactory.Verify(f => f.CreateLogger("category"), Times.Once); + } +} diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs index 9f9a42a4de..8c6aed9105 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs @@ -121,6 +121,33 @@ public void OfType_Should_Return_CorrectObject() Assert.AreEqual(PassedTestNodeStateProperty.CachedInstance, property.OfType().Single()); Assert.HasCount(2, property.OfType()); + + // No DummyProperty2 in the bag — exercises the "no match found" path in the while-loop + Assert.IsEmpty(property.OfType()); + } + + [TestMethod] + public void OfType_WithSingleMatch_ReturnsSingleItemArray() + { + PropertyBag property = new(); + DummyProperty singleProperty = new(); + property.Add(singleProperty); + property.Add(PassedTestNodeStateProperty.CachedInstance); + + DummyProperty[] result = property.OfType(); + + Assert.HasCount(1, result); + Assert.AreSame(singleProperty, result[0]); + } + + [TestMethod] + public void OfType_WithOnlyTestNodeStateProperty_ReturnsEmpty() + { + PropertyBag property = new(); + property.Add(PassedTestNodeStateProperty.CachedInstance); + + // _property is null; _testNodeStateProperty is set — exercises the new early-return path + Assert.IsEmpty(property.OfType()); } [TestMethod] diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs index ebfdbdbb22..78f7406778 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs @@ -328,4 +328,18 @@ public void MatchAllFilterSubpathWithPropertyExpression() [TestMethod] public void MatchAllFilterWithPropertyExpression_DoNotAllowInMiddleOfFilter() => Assert.ThrowsExactly(() => _ = new TreeNodeFilter("/**/Path[A=B]")); + + [DataRow("/**", false)] + [DataRow("/A/B", false)] + [DataRow("/(A|B)", false)] + [DataRow("/(A&B)", false)] + [DataRow("/*.UnitTests[Tag=Fast]", true)] + [DataRow("/**[A=B]", true)] + [DataRow("/(A[Tag=Fast]&B)", true)] + [TestMethod] + public void ContainsPropertyFilters_ReturnsExpectedValue(string filterExpression, bool expected) + { + TreeNodeFilter filter = new(filterExpression); + Assert.AreEqual(expected, filter.ContainsPropertyFilters); + } } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs index bfc8c49a4c..bf303d7b29 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs @@ -48,7 +48,7 @@ public async Task ServerCanBeStartedAndAborted_TcpIp() ITestApplicationCancellationTokenSource stopService = testApplication.ServiceProvider.GetTestApplicationCancellationTokenSource(); stopService.Cancel(); - Assert.AreEqual(ExitCodes.TestSessionAborted, await serverTask); + Assert.AreEqual((int)ExitCode.TestSessionAborted, await serverTask); } [TestMethod] diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs index 918e2286a5..0fbb033586 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs @@ -31,7 +31,7 @@ public async Task GetProcessExitCodeAsync_If_All_Skipped_Returns_ZeroTestsRan() Properties = new PropertyBag(SkippedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.ZeroTests, _testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.ZeroTests, _testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -46,7 +46,7 @@ public async Task GetProcessExitCodeAsync_If_No_Tests_Ran_Returns_ZeroTestsRan() Properties = new PropertyBag(), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.ZeroTests, _testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.ZeroTests, _testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -62,7 +62,7 @@ public async Task GetProcessExitCodeAsync_If_Failed_Tests_Returns_AtLeastOneTest Properties = new PropertyBag(testNodeStateProperty), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.AtLeastOneTestFailed, _testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.AtLeastOneTestFailed, _testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -91,7 +91,7 @@ TestApplicationResult testApplicationResult Properties = new PropertyBag(), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.TestSessionAborted, testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.TestSessionAborted, testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -107,7 +107,7 @@ public async Task GetProcessExitCodeAsync_If_TestAdapter_Returns_TestAdapterTest Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.TestAdapterTestSessionFailure, _testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.TestAdapterTestSessionFailure, _testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -139,7 +139,7 @@ TestApplicationResult testApplicationResult Properties = new PropertyBag(InProgressTestNodeStateProperty.CachedInstance), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.MinimumExpectedTestsPolicyViolation, testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.MinimumExpectedTestsPolicyViolation, testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -159,7 +159,7 @@ TestApplicationResult testApplicationResult DisplayName = "DisplayName", }), CancellationToken.None); - Assert.AreEqual(ExitCodes.ZeroTests, testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.ZeroTests, testApplicationResult.GetProcessExitCode()); } [TestMethod] @@ -180,20 +180,20 @@ TestApplicationResult testApplicationResult Properties = new PropertyBag(DiscoveredTestNodeStateProperty.CachedInstance), }), CancellationToken.None); - Assert.AreEqual(ExitCodes.Success, testApplicationResult.GetProcessExitCode()); + Assert.AreEqual((int)ExitCode.Success, testApplicationResult.GetProcessExitCode()); } - [DataRow("8", ExitCodes.Success)] - [DataRow("8;2", ExitCodes.Success)] - [DataRow("8;", ExitCodes.Success)] - [DataRow("8;2;", ExitCodes.Success)] - [DataRow("5", ExitCodes.ZeroTests)] - [DataRow("5;7", ExitCodes.ZeroTests)] - [DataRow("5;", ExitCodes.ZeroTests)] - [DataRow("5;7;", ExitCodes.ZeroTests)] - [DataRow(";", ExitCodes.ZeroTests)] - [DataRow(null, ExitCodes.ZeroTests)] - [DataRow("", ExitCodes.ZeroTests)] + [DataRow("8", (int)ExitCode.Success)] + [DataRow("8;2", (int)ExitCode.Success)] + [DataRow("8;", (int)ExitCode.Success)] + [DataRow("8;2;", (int)ExitCode.Success)] + [DataRow("5", (int)ExitCode.ZeroTests)] + [DataRow("5;7", (int)ExitCode.ZeroTests)] + [DataRow("5;", (int)ExitCode.ZeroTests)] + [DataRow("5;7;", (int)ExitCode.ZeroTests)] + [DataRow(";", (int)ExitCode.ZeroTests)] + [DataRow(null, (int)ExitCode.ZeroTests)] + [DataRow("", (int)ExitCode.ZeroTests)] [TestMethod] public void GetProcessExitCodeAsync_IgnoreExitCodes(string? argument, int expectedExitCode) { From 0cc145cd2ad2c178ce012977b111d9b9cda60a2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:48:31 +0000 Subject: [PATCH 5/7] Merge latest main and resolve conflicts Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/9866530f-a95f-4703-8378-d7ae99ad9510 Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- formal-verification/TARGETS.md | 16 +- ...arser_parseoptionandseparators_informal.md | 215 ++++++++++++++++++ 2 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 formal-verification/specs/commandlineparser_parseoptionandseparators_informal.md diff --git a/formal-verification/TARGETS.md b/formal-verification/TARGETS.md index bc67adc214..d3b9219072 100644 --- a/formal-verification/TARGETS.md +++ b/formal-verification/TARGETS.md @@ -18,19 +18,19 @@ |---|------|------|-------|--------|----------| | 1 | `ArgumentArity` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ArgumentArity.cs` | 2 | Informal spec extracted | [PR #7799](https://github.com/microsoft/testfx/pull/7799) | | 2 | `CommandLineParser.TryUnescape` | `src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs` | 2 | Informal spec extracted | — | -| 3 | `CommandLineParser.ParseOptionAndSeparators` | `src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs` | 1 | Identified | — | +| 3 | `CommandLineParser.ParseOptionAndSeparators` | `src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs` | 2 | Informal spec extracted | — | | 4 | `CommandLineOptionsValidator` arity validation | `src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs` | 1 | Identified | — | | 5 | `CommandLineParseResult.Equals` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ParseResult.cs` | 1 | Identified | — | -| 6 | `ResponseFileHelper.SplitCommandLine` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ResponseFileHelper.cs` | 2 | Informal spec extracted | — | -| 7 | `TreeNodeFilter.MatchFilterPattern` | `src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs` | 1 | Identified | — | +| 6 | `ResponseFileHelper.SplitCommandLine` | `src/Platform/Microsoft.Testing.Platform/CommandLine/ResponseFileHelper.cs` | 2 | Informal spec extracted | [PR #7899](https://github.com/microsoft/testfx/pull/7899) | +| 7 | `TreeNodeFilter.MatchFilterPattern` | `src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs` | 2 | Informal spec extracted | — | ## Priority Order -1. **`ArgumentArity`** — highest priority. Smallest self-contained target; decidable properties; good warm-up for setting up the Lean environment. Informal spec done. -2. **`CommandLineParser.TryUnescape`** — second priority. Pure function with clear specification; security-relevant string processing. Informal spec extracted (PR open). -3. **`TreeNodeFilter.MatchFilterPattern`** — **elevated third priority**. Pure recursive Boolean algebra; structural induction proofs; De Morgan and double negation provable by `simp`. Excellent Lean target. -4. **`ResponseFileHelper.SplitCommandLine`** — fourth priority. Pure tokeniser with state machine; clear grammar-based properties. -5. **`CommandLineParser.ParseOptionAndSeparators`** — fifth priority. Small pure function; useful for verifying parser correctness. +1. **`ArgumentArity`** — highest priority. Smallest self-contained target; decidable properties; good warm-up for setting up the Lean environment. Informal spec done. **Next: Task 3 (blocked by Lean toolchain).** +2. **`CommandLineParser.TryUnescape`** — second priority. Pure function with clear specification; security-relevant string processing. Informal spec extracted. **Next: Task 3 (blocked by Lean toolchain).** +3. **`TreeNodeFilter.MatchFilterPattern`** — **elevated third priority**. Pure recursive Boolean algebra; structural induction proofs; De Morgan and double negation provable by `simp`. Informal spec extracted. **Next: Task 3 (blocked by Lean toolchain).** +4. **`ResponseFileHelper.SplitCommandLine`** — fourth priority. Pure tokeniser with state machine; clear grammar-based properties. Informal spec extracted (PR open). **Next: Task 3 (blocked by Lean toolchain).** +5. **`CommandLineParser.ParseOptionAndSeparators`** — fifth priority. Small pure function; useful for verifying parser correctness. Informal spec extracted this run. **Next: Task 3 (blocked by Lean toolchain).** 6. **`CommandLineOptionsValidator` arity validation** — sixth priority. Validation logic with clear input/output contract. 7. **`CommandLineParseResult.Equals`** — seventh priority. Structural equality; good for verifying equivalence-relation laws. diff --git a/formal-verification/specs/commandlineparser_parseoptionandseparators_informal.md b/formal-verification/specs/commandlineparser_parseoptionandseparators_informal.md new file mode 100644 index 0000000000..47cfbbd163 --- /dev/null +++ b/formal-verification/specs/commandlineparser_parseoptionandseparators_informal.md @@ -0,0 +1,215 @@ +# Informal Specification — `CommandLineParser.ParseOptionAndSeparators` + +> 🔬 **Lean Squad** — auto-generated and maintained by the Lean Squad FV agent. + +## Target + +- **Type**: `static void ParseOptionAndSeparators(string arg, out string? currentOption, out string? currentArg)` (local function inside `CommandLineParser.Parse`) +- **Namespace**: `Microsoft.Testing.Platform.CommandLine` +- **File**: `src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs` +- **Phase**: 2 — Informal Spec +- **Related spec**: `commandlineparser_tryunescape_informal.md` + +--- + +## Purpose + +`ParseOptionAndSeparators` splits a single command-line token that starts with `-` or `--` into: + +1. An **option name** (`currentOption`) — the part before the first `:` or `=` delimiter, with leading dashes stripped. +2. An **inline value** (`currentArg`) — the part after the delimiter, or `null` if no delimiter is present. + +This implements the convention that allows options to be supplied in two styles: + +- **Separate token style**: `--option value` — handled by the caller; `ParseOptionAndSeparators` gets `"--option"` and returns `("option", null)`. +- **Inline style**: `--option=value` or `--option:value` — `ParseOptionAndSeparators` gets the combined token and splits it. + +The function is a local `static` method: it accesses no state, has no side effects, and is a pure string transformation. + +--- + +## Data Model + +``` +ParseOptionAndSeparators : string → (string × string?) +ParseOptionAndSeparators(arg) = (currentOption, currentArg) +``` + +where: + +- `currentOption : string` — never null; contains no leading `-` characters +- `currentArg : string?` — null if no delimiter; possibly empty string `""` if delimiter is the last character + +--- + +## Preconditions + +1. `arg` is not null (enforced by the caller's loop variable). +2. `arg` has been validated by the caller to match one of: + - **Single-dash form**: `arg.Length > 1 ∧ arg[0] = '-' ∧ arg[1] ≠ '-'` + - **Double-dash form**: `arg.Length > 2 ∧ arg[0] = '-' ∧ arg[1] = '-' ∧ arg[2] ≠ '-'` + + In both cases, `arg` starts with `-` but does NOT start with `---`. + +3. `arg.Length ≥ 2` by the above preconditions. + +> **Note**: The function itself does not check these preconditions. It accepts any non-null `string` as input. Callers outside `Parse` may pass unusual inputs. + +--- + +## Algorithm (Reference Implementation) + +```csharp +(currentOption, currentArg) = arg.IndexOfAny([':', '=']) switch +{ + -1 => (arg, null), + var delimiterIndex => (arg[..delimiterIndex], arg[(delimiterIndex + 1)..]), +}; +currentOption = currentOption.TrimStart('-'); +``` + +Steps: +1. Find `delimiterIndex = arg.IndexOfAny([':', '='])` — the 0-based index of the FIRST occurrence of `:` or `=` in `arg` (or −1 if absent). +2. **No delimiter** (`delimiterIndex = -1`): `currentOption := arg`, `currentArg := null`. +3. **Delimiter found** (`delimiterIndex ≥ 0`): `currentOption := arg[0..delimiterIndex)`, `currentArg := arg[delimiterIndex+1..]`. +4. Strip leading dashes: `currentOption := currentOption.TrimStart('-')`. + +--- + +## Postconditions / Properties + +### Property Group 1 — Delimiter-free case + +1. **No delimiter → null arg**: If `arg` contains no `:` and no `=`, then `currentArg = null`. +2. **No delimiter → full option**: If `arg` contains no `:` and no `=`, then `currentOption = arg.TrimStart('-')`. +3. **No dashes in option**: `currentOption` has no leading `-` in all cases (TrimStart removes them). + +### Property Group 2 — Delimiter present case + +4. **Delimiter → non-null arg**: If `arg` contains `:` or `=`, then `currentArg ≠ null`. +5. **First delimiter only**: Let `i = arg.IndexOfAny([':', '='])`. Then `currentOption = arg[..i].TrimStart('-')` and `currentArg = arg[(i+1)..]`. +6. **Value may be empty**: `currentArg` may be `""` when the delimiter is the last character (`i = arg.Length - 1`). +7. **Value preserves subsequent delimiters**: Characters after the first delimiter are preserved verbatim in `currentArg`. In particular, `currentArg` may itself contain `:` or `=`. + +### Property Group 3 — Structural / Reconstructibility + +8. **Option contains no delimiters**: `currentOption` contains no `:` and no `=` character. (Because we split at the first occurrence and then `TrimStart` only removes `-`.) +9. **Lossless split (with delimiter)**: If `delimiterIndex ≥ 0`, let `prefix = arg[..delimiterIndex]`. Then: + - `currentOption = prefix.TrimStart('-')` + - `currentArg = arg[(delimiterIndex + 1)..]` + - `prefix = ('-' × k) + currentOption` for some `k ≥ 0` (exactly the number of leading dashes in `prefix`) + - `arg = prefix + arg[delimiterIndex] + currentArg` + - Therefore `arg.Length = prefix.Length + 1 + currentArg.Length` +10. **Lossless split (no delimiter)**: If `delimiterIndex = -1`, then: + - `currentOption = arg.TrimStart('-')` + - `arg = ('-' × k) + currentOption` for some `k ≥ 0` + +### Property Group 4 — Determinism and independence + +11. **Purity**: `ParseOptionAndSeparators` is a pure function — same input always yields same output. +12. **Delimiter priority is lexicographic order**: `IndexOfAny` finds the leftmost occurrence of either delimiter character. The type of delimiter (`:` vs `=`) is irrelevant; only position matters. +13. **Which delimiter came first determines the split**: If `arg = "x=y:z"`, then `currentOption = "x"` and `currentArg = "y:z"`. If `arg = "x:y=z"`, then `currentOption = "x"` and `currentArg = "y=z"`. + +### Property Group 5 — Edge cases + +14. **Option can be empty**: If `arg` starts with `:` or `=` (violates preconditions but valid input to function), then `currentOption = ""`. +15. **Option prefix is entirely dashes**: If `arg = "--:value"` (double-dash immediately followed by delimiter), `currentOption = ""` (all dashes stripped), `currentArg = "value"`. +16. **Delimiter immediately after prefix**: If `arg = "--opt="` (delimiter is last char), `currentOption = "opt"`, `currentArg = ""`. + +--- + +## Edge Cases + +| Input `arg` | `currentOption` | `currentArg` | Notes | +|-------------|-----------------|--------------|-------| +| `"--option1"` | `"option1"` | `null` | Standard no-value form | +| `"-option1"` | `"option1"` | `null` | Single-dash form | +| `"--option1:a"` | `"option1"` | `"a"` | Colon delimiter | +| `"--option1=a"` | `"option1"` | `"a"` | Equals delimiter | +| `"--option1=a=a"` | `"option1"` | `"a=a"` | Second `=` preserved in value | +| `"--option1:a:a"` | `"option1"` | `"a:a"` | Second `:` preserved in value | +| `"--option1:a=a"` | `"option1"` | `"a=a"` | `:` wins (leftmost) | +| `"--option1=a:a"` | `"option1"` | `"a:a"` | `=` wins (leftmost) | +| `"--option1="` | `"option1"` | `""` | Empty value (delimiter at end) | +| `"--option1:"` | `"option1"` | `""` | Empty value (delimiter at end) | +| `"--:"` | `""` | `""` | Empty option AND empty value | +| `"-a"` | `"a"` | `null` | Single-dash, no delimiter | +| `"---option1"` | Not called (rejected by caller) | | Three dashes: caller produces error | + +--- + +## Invariants + +1. `currentOption` is never `null` — it is always a `string` (possibly empty). +2. `currentArg` is `null` if and only if no delimiter character appears in `arg`. +3. The function never throws (no index-out-of-bounds; `arg[..delimiterIndex]` with `delimiterIndex ≥ 0` is always valid; `arg[(delimiterIndex + 1)..]` when `delimiterIndex = arg.Length - 1` yields `""`). +4. `currentOption` contains no `:` and no `=`. +5. `currentOption` has no leading `-` characters. + +--- + +## Inferred Design Intent + +The function implements the `--key=value` and `--key:value` option-argument delimiter convention documented in `dotnet/command-line-api` and the [System.CommandLine syntax docs](https://learn.microsoft.com/dotnet/standard/commandline/syntax#option-argument-delimiters). The two-delimiter support (both `:` and `=`) mirrors the Windows cmd and .NET CLI conventions. + +Only the FIRST delimiter counts — subsequent occurrences pass through verbatim into the value. This enables values like `--connection=Server=tcp:localhost,1433` (a connection string containing both `=` and `:`). + +The `TrimStart('-')` is applied uniformly to the option-name prefix, normalizing both `-opt` and `--opt` forms to `opt`. + +--- + +## Potential Issues / Open Questions + +### Issue 1 — Empty option name not rejected +**Observation**: The function does not validate that `currentOption` is non-empty after `TrimStart('-')`. Inputs like `"--:value"` or `"--="` produce `currentOption = ""`, which is then treated as a valid option name by the caller. The caller does not re-validate. + +**Consequence**: The parser silently creates a `CommandLineParseOption` with option name `""` instead of reporting an error. Attempting to look up this option in the registered options table will fail to find a match (but may produce an unhelpful error message downstream). + +**Severity**: Minor / edge case. In practice, callers pass well-formed command lines. The preconditions exclude `--:` inputs via the `arg[2] != '-'` check only if we consider `:` as a non-dash character — which it is — but the preconditions do NOT exclude `--:` specifically. + +**Recommendation**: Add a post-split guard: `if (string.IsNullOrEmpty(currentOption)) { errors.Add(...); currentOption = null; }`. + +### Open Question 1 — Interaction with TryUnescape +`currentArg` (when non-null) is passed to `TryUnescape` after `.Trim()`. The `Trim()` strips whitespace. The interaction between the inline value and quoting/unescaping is not directly tested: does `--option='hello world'` work? According to the source, it should, because `currentArg = "'hello world'"` which is then unescaped by `TryUnescape`. + +### Open Question 2 — Delimiter within quoted value +`"--option='a:b'"` produces `currentOption = "option"` and `currentArg = "'a:b'"`. The colon is inside quotes, but `ParseOptionAndSeparators` does NOT understand quoting — it splits on the first raw `:`. For `"--option:a"`, split occurs before seeing quotes. For `"--option='a:b'"`, the first `:` is index 9 (before `'a`), so `currentArg = "'a:b'"`. Wait — actually `"--option='a:b'"` has `=` at index 8, not `:` first. So `currentOption = "option"`, `currentArg = "'a:b'"`. This is correct. But `"--option:'a:b'"` would split at the first `:` (index 8), giving `currentOption = "option"` and `currentArg = "'a:b'"`. The second `:` is preserved in the value. This seems correct. + +--- + +## Approximations for Lean Model + +1. **Model strings as `List Char`** (or `String` which is `List Char` in Lean). Character comparisons are decidable. +2. **Model `TrimStart('-')`** as `List.dropWhile (· == '-')`. +3. **Model `IndexOfAny([':', '='])`** as `List.findIdx? (fun c => c == ':' || c == '=')` (returns `Option Nat`). +4. **Model return value** as `String × Option String` (option name × optional inline value). +5. **Do NOT model**: threading, exception behaviour, null reference semantics, caller validation. +6. **Decidable propositions**: all properties in Groups 1–5 are decidable for concrete inputs; `decide` should close concrete test cases. The structural properties (Groups 3, 5) require induction on `List.dropWhile` and `List.findIdx?`. +7. **Key lemma to prove**: `List.findIdx? (fun c => c == ':' || c == '=') s = none ↔ ¬ ∃ c ∈ s, c == ':' || c == '='`. +8. **Key lemma 2**: If `findIdx? p s = some i`, then `s[i]` satisfies `p` and no `s[j]` with `j < i` satisfies `p`. + +--- + +## Examples for Lean + +```lean +-- No delimiter +#eval parseOptionAndSeparators "--option1" +-- Expected: ("option1", none) + +-- Colon delimiter +#eval parseOptionAndSeparators "--option1:a" +-- Expected: ("option1", some "a") + +-- Equals delimiter, value contains colon +#eval parseOptionAndSeparators "--option1=a:a" +-- Expected: ("option1", some "a:a") + +-- Double equals, only first counts +#eval parseOptionAndSeparators "--option1=a=a" +-- Expected: ("option1", some "a=a") + +-- Trailing delimiter → empty value +#eval parseOptionAndSeparators "--option1=" +-- Expected: ("option1", some "") +``` From 61a2df427c497967de011294fe9c67dd504621d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 30 Apr 2026 15:54:10 +0200 Subject: [PATCH 6/7] Fix test expectations for code fixer - Test 2: Use fully-qualified [System.ComponentModel.Description] in input to avoid CS0104 ambiguity when both namespaces are imported - Tests 2 & 3: Expect 'Description' instead of 'DescriptionAttribute' since Roslyn Simplifier strips the Attribute suffix --- .../DoNotUseSystemDescriptionAttributeAnalyzerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index 4ed0c077a5..b8e88b2e19 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -55,7 +55,7 @@ public async Task WhenTestMethodHasSystemDescriptionAttributeWithSystemComponent public class MyTestClass { [TestMethod] - [Description("Description")] + [System.ComponentModel.Description("Description")] public void [|MyTestMethod|]() { } @@ -70,7 +70,7 @@ public class MyTestClass public class MyTestClass { [TestMethod] - [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Description")] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Description")] public void MyTestMethod() { } @@ -100,7 +100,7 @@ public class MyTestClass public class MyTestClass { [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] - [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Description")] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Description")] public void MyTestMethod() { } From 50687767a5df3d5dfb7aa54cb85f8162cd3b9757 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 14:35:06 +0000 Subject: [PATCH 7/7] Fix failing test: short-form Description cannot be ambiguous in test input Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- ...NotUseSystemDescriptionAttributeAnalyzerTests.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index 6ff45298e4..23c2ea4048 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -113,14 +113,16 @@ public void MyTestMethod() [TestMethod] public async Task WhenTestMethodHasShortFormDescriptionAttributeWithSystemComponentModelUsing_UsesFullyQualifiedMSTestDescription() { + // When only System.ComponentModel is imported, the short form [Description] unambiguously + // resolves to System.ComponentModel.DescriptionAttribute. The fixer must produce the + // fully-qualified MSTest form since no MSTest using is present. string code = """ using System.ComponentModel; - using Microsoft.VisualStudio.TestTools.UnitTesting; - [TestClass] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] public class MyTestClass { - [TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] [Description("Description")] public void [|MyTestMethod|]() { @@ -130,12 +132,11 @@ public class MyTestClass string fixedCode = """ using System.ComponentModel; - using Microsoft.VisualStudio.TestTools.UnitTesting; - [TestClass] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] public class MyTestClass { - [TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Description")] public void MyTestMethod() {